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 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
366 if (evt instanceof IdleStateEvent) {
367 IdleStateEvent e = (IdleStateEvent) evt;
368 // If camera does not use the channel for X amount of time it will close.
369 if (e.state() == IdleState.READER_IDLE) {
370 String urlToKeepOpen = "";
371 switch (thing.getThingTypeUID().getId()) {
373 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
376 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
379 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
380 if (channelTracking != null) {
381 if (channelTracking.getChannel() == ctx.channel()) {
382 return; // don't auto close this as it is for the alarms.
385 logger.debug("Closing an idle channel for camera:{}", cameraConfig.getIp());
392 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
393 IpCameraDynamicStateDescriptionProvider stateDescriptionProvider, HttpService httpService) {
395 this.stateDescriptionProvider = stateDescriptionProvider;
396 if (ipAddress != null) {
399 hostIp = Helper.getLocalIpAddress();
401 this.groupTracker = groupTracker;
402 this.httpService = httpService;
405 private IpCameraHandler getHandle() {
409 // false clears the stored user/pass hash, true creates the hash
410 public boolean setBasicAuth(boolean useBasic) {
412 logger.debug("Clearing out the stored BASIC auth now.");
415 } else if (!basicAuth.isEmpty()) {
416 // If the binding sends multiple requests before basicAuth was set, this may trigger falsely.
417 logger.warn("Camera is reporting your username and/or password is wrong.");
420 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
421 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
422 ByteBuf byteBuf = null;
424 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
425 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
427 if (byteBuf != null) {
433 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
438 private String getCorrectUrlFormat(String longUrl) {
439 String temp = longUrl;
442 if (longUrl.isEmpty() || "ffmpeg".equals(longUrl)) {
447 url = new URL(longUrl);
448 int port = url.getPort();
450 if (url.getQuery() == null) {
451 temp = url.getPath();
453 temp = url.getPath() + "?" + url.getQuery();
456 if (url.getQuery() == null) {
457 temp = ":" + url.getPort() + url.getPath();
459 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
462 } catch (MalformedURLException e) {
463 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
468 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
469 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
470 sendHttpRequest("PUT", httpRequestURL, null);
473 public void sendHttpGET(String httpRequestURL) {
474 sendHttpRequest("GET", httpRequestURL, null);
477 public int getPortFromShortenedUrl(String httpRequestURL) {
478 if (httpRequestURL.startsWith(":")) {
479 int end = httpRequestURL.indexOf("/");
480 return Integer.parseInt(httpRequestURL.substring(1, end));
482 return cameraConfig.getPort();
485 public String getTinyUrl(String httpRequestURL) {
486 if (httpRequestURL.startsWith(":")) {
487 int beginIndex = httpRequestURL.indexOf("/");
488 return httpRequestURL.substring(beginIndex);
490 return httpRequestURL;
493 private void checkCameraConnection() {
494 if (snapshotUri.isEmpty() || snapshotPolling) {
495 // Already polling or camera has RTSP only and no HTTP server
498 Bootstrap localBootstrap = mainBootstrap;
499 if (localBootstrap != null) {
500 ChannelFuture chFuture = localBootstrap
501 .connect(new InetSocketAddress(cameraConfig.getIp(), cameraConfig.getPort()));
502 if (chFuture.awaitUninterruptibly(500)) {
503 chFuture.channel().close();
507 cameraCommunicationError(
508 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
511 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
512 // The authHandler will generate a digest string and re-send using this same function when needed.
513 @SuppressWarnings("null")
514 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
515 int port = getPortFromShortenedUrl(httpRequestURLFull);
516 String httpRequestURL = getTinyUrl(httpRequestURLFull);
518 if (mainBootstrap == null) {
519 mainBootstrap = new Bootstrap();
520 mainBootstrap.group(mainEventLoopGroup);
521 mainBootstrap.channel(NioSocketChannel.class);
522 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
523 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
524 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
525 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
526 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
527 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
530 public void initChannel(SocketChannel socketChannel) throws Exception {
531 // HIK Alarm stream needs > 9sec idle to stop stream closing
532 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
533 socketChannel.pipeline().addLast(new HttpClientCodec());
534 socketChannel.pipeline().addLast(AUTH_HANDLER,
535 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
536 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
538 switch (thing.getThingTypeUID().getId()) {
540 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
543 socketChannel.pipeline()
544 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
547 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
550 socketChannel.pipeline().addLast(
551 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
553 case HIKVISION_THING:
554 socketChannel.pipeline()
555 .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
558 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
561 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
568 FullHttpRequest request;
569 if (!"PUT".equals(httpMethod) || (useDigestAuth && digestString == null)) {
570 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
571 request.headers().set("Host", cameraConfig.getIp() + ":" + port);
572 request.headers().set("Connection", HttpHeaderValues.KEEP_ALIVE);
574 request = putRequestWithBody;
577 if (!basicAuth.isEmpty()) {
579 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
582 request.headers().set("Authorization", "Basic " + basicAuth);
587 if (digestString != null) {
588 request.headers().set("Authorization", "Digest " + digestString);
592 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
593 .addListener(new ChannelFutureListener() {
596 public void operationComplete(@Nullable ChannelFuture future) {
597 if (future == null) {
600 if (future.isDone() && future.isSuccess()) {
601 Channel ch = future.channel();
602 openChannels.add(ch);
606 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
609 openChannel(ch, httpRequestURL);
610 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
611 commonHandler.setURL(httpRequestURLFull);
612 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
613 authHandler.setURL(httpMethod, httpRequestURL);
615 switch (thing.getThingTypeUID().getId()) {
617 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
618 amcrestHandler.setURL(httpRequestURL);
621 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
622 instarHandler.setURL(httpRequestURL);
625 ch.writeAndFlush(request);
626 } else { // an error occured
627 cameraCommunicationError(
628 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
634 public void processSnapshot(byte[] incommingSnapshot) {
635 lockCurrentSnapshot.lock();
637 currentSnapshot = incommingSnapshot;
638 if (cameraConfig.getGifPreroll() > 0) {
639 fifoSnapshotBuffer.add(incommingSnapshot);
640 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
641 fifoSnapshotBuffer.removeFirst();
645 lockCurrentSnapshot.unlock();
646 currentSnapshotTime = Instant.now();
649 if (updateImageChannel) {
650 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
651 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
652 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
653 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
654 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
655 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
656 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
660 public void startStreamServer() {
661 servlet = new CameraServlet(this, httpService);
662 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
663 + getThing().getUID().getId() + "/ipcamera.m3u8"));
664 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
665 + getThing().getUID().getId() + "/ipcamera.jpg"));
666 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
667 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
670 public void openCamerasStream() {
671 closeChannel(getTinyUrl(mjpegUri));
672 mainEventLoopGroup.schedule(this::openMjpegStream, 0, TimeUnit.MILLISECONDS);
675 private void openMjpegStream() {
676 sendHttpGET(mjpegUri);
679 private void openChannel(Channel channel, String httpRequestURL) {
680 ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
681 if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
682 tracker.setChannel(channel);
685 channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
688 public void closeChannel(String url) {
689 ChannelTracking channelTracking = channelTrackingMap.get(url);
690 if (channelTracking != null) {
691 if (channelTracking.getChannel().isOpen()) {
692 channelTracking.getChannel().close();
699 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
700 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
703 private void cleanChannels() {
704 for (Channel channel : openChannels) {
705 boolean oldChannel = true;
706 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
707 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
708 channelTrackingMap.remove(channelTracking.getRequestUrl());
710 if (channelTracking.getChannel() == channel) {
711 logger.debug("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
721 public void storeHttpReply(String url, String content) {
722 ChannelTracking channelTracking = channelTrackingMap.get(url);
723 if (channelTracking != null) {
724 channelTracking.setReply(content);
728 private void storeSnapshots() {
730 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
731 lockCurrentSnapshot.lock();
733 for (byte[] foo : fifoSnapshotBuffer) {
734 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
737 OutputStream fos = new FileOutputStream(file);
740 } catch (FileNotFoundException e) {
741 logger.warn("FileNotFoundException {}", e.getMessage());
742 } catch (IOException e) {
743 logger.warn("IOException {}", e.getMessage());
747 lockCurrentSnapshot.unlock();
751 public void setupFfmpegFormat(FFmpegFormat format) {
752 String inputOptions = cameraConfig.getFfmpegInputOptions();
753 if (cameraConfig.getFfmpegOutput().isEmpty()) {
754 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
757 if (rtspUri.isEmpty()) {
758 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
761 if (cameraConfig.getFfmpegLocation().isEmpty()) {
762 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
765 if (rtspUri.toLowerCase().contains("rtsp")) {
766 if (inputOptions.isEmpty()) {
767 inputOptions = "-rtsp_transport tcp";
771 // Make sure the folder exists, if not create it.
772 new File(cameraConfig.getFfmpegOutput()).mkdirs();
775 if (ffmpegHLS == null) {
776 if (!inputOptions.isEmpty()) {
777 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
778 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
779 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
780 cameraConfig.getUser(), cameraConfig.getPassword());
782 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
783 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
784 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
785 cameraConfig.getPassword());
788 Ffmpeg localHLS = ffmpegHLS;
789 if (localHLS != null) {
790 localHLS.startConverting();
794 if (cameraConfig.getGifPreroll() > 0) {
795 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
796 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
797 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
798 + cameraConfig.getGifOutOptions(),
799 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
800 cameraConfig.getPassword());
802 if (!inputOptions.isEmpty()) {
803 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
805 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
807 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
808 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
809 cameraConfig.getUser(), cameraConfig.getPassword());
811 if (cameraConfig.getGifPreroll() > 0) {
814 Ffmpeg localGIF = ffmpegGIF;
815 if (localGIF != null) {
816 localGIF.startConverting();
817 if (gifHistory.isEmpty()) {
818 gifHistory = gifFilename;
819 } else if (!"ipcamera".equals(gifFilename)) {
820 gifHistory = gifFilename + "," + gifHistory;
821 if (gifHistoryLength > 49) {
822 int endIndex = gifHistory.lastIndexOf(",");
823 gifHistory = gifHistory.substring(0, endIndex);
826 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
830 if (!inputOptions.isEmpty()) {
831 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
833 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
835 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
836 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
837 cameraConfig.getUser(), cameraConfig.getPassword());
838 Ffmpeg localRecord = ffmpegRecord;
839 if (localRecord != null) {
840 localRecord.startConverting();
841 if (mp4History.isEmpty()) {
842 mp4History = mp4Filename;
843 } else if (!"ipcamera".equals(mp4Filename)) {
844 mp4History = mp4Filename + "," + mp4History;
845 if (mp4HistoryLength > 49) {
846 int endIndex = mp4History.lastIndexOf(",");
847 mp4History = mp4History.substring(0, endIndex);
851 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
854 Ffmpeg localAlarms = ffmpegRtspHelper;
855 if (localAlarms != null) {
856 localAlarms.stopConverting();
857 if (!audioAlarmEnabled && !motionAlarmEnabled) {
861 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
862 String filterOptions = "";
863 if (!audioAlarmEnabled) {
864 filterOptions = "-an";
866 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
868 if (!motionAlarmEnabled && !ffmpegSnapshotGeneration) {
869 filterOptions = filterOptions.concat(" -vn");
870 } else if (motionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
871 String usersMotionOptions = cameraConfig.getMotionOptions();
872 if (usersMotionOptions.startsWith("-")) {
873 // Need to put the users custom options first in the chain before the motion is detected
874 filterOptions += " " + usersMotionOptions + ",select='gte(scene,"
875 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
877 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
878 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
880 } else if (motionAlarmEnabled) {
881 filterOptions = filterOptions.concat(" -vf select='gte(scene,"
882 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print");
884 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
885 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
886 localAlarms = ffmpegRtspHelper;
887 if (localAlarms != null) {
888 localAlarms.startConverting();
892 if (ffmpegMjpeg == null) {
893 if (inputOptions.isEmpty()) {
894 inputOptions = "-hide_banner -loglevel warning";
896 inputOptions += " -hide_banner -loglevel warning";
898 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
899 cameraConfig.getMjpegOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
900 + getThing().getUID().getId() + "/ipcamera.jpg",
901 cameraConfig.getUser(), cameraConfig.getPassword());
903 Ffmpeg localMjpeg = ffmpegMjpeg;
904 if (localMjpeg != null) {
905 localMjpeg.startConverting();
909 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
910 if (ffmpegSnapshot == null) {
911 if (inputOptions.isEmpty()) {
913 inputOptions = "-threads 1 -skip_frame nokey -hide_banner -loglevel warning";
915 inputOptions += " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
917 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
918 cameraConfig.getSnapshotOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
919 + getThing().getUID().getId() + "/snapshot.jpg",
920 cameraConfig.getUser(), cameraConfig.getPassword());
922 Ffmpeg localSnaps = ffmpegSnapshot;
923 if (localSnaps != null) {
924 localSnaps.startConverting();
930 public void noMotionDetected(String thisAlarmsChannel) {
931 setChannelState(thisAlarmsChannel, OnOffType.OFF);
932 firstMotionAlarm = false;
933 motionAlarmUpdateSnapshot = false;
934 motionDetected = false;
935 if (streamingAutoFps) {
936 stopSnapshotPolling();
937 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
938 stopSnapshotPolling();
943 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
944 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
945 * tampering with the camera.
947 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
948 updateState(thisAlarmsChannel, state);
951 public void motionDetected(String thisAlarmsChannel) {
952 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
953 updateState(thisAlarmsChannel, OnOffType.ON);
954 motionDetected = true;
955 if (streamingAutoFps) {
956 startSnapshotPolling();
958 if (cameraConfig.getUpdateImageWhen().contains("2")) {
959 if (!firstMotionAlarm) {
960 if (!snapshotUri.isEmpty()) {
963 firstMotionAlarm = true;// reset back to false when the jpg arrives.
965 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
966 if (!snapshotPolling) {
967 startSnapshotPolling();
969 firstMotionAlarm = true;
970 motionAlarmUpdateSnapshot = true;
974 public void audioDetected() {
975 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
976 if (cameraConfig.getUpdateImageWhen().contains("3")) {
977 if (!firstAudioAlarm) {
978 if (!snapshotUri.isEmpty()) {
981 firstAudioAlarm = true;// reset back to false when the jpg arrives.
983 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
984 firstAudioAlarm = true;
985 audioAlarmUpdateSnapshot = true;
989 public void noAudioDetected() {
990 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
991 firstAudioAlarm = false;
992 audioAlarmUpdateSnapshot = false;
995 public void recordMp4(String filename, int seconds) {
996 mp4Filename = filename;
997 mp4RecordTime = seconds;
998 setupFfmpegFormat(FFmpegFormat.RECORD);
999 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1002 public void recordGif(String filename, int seconds) {
1003 gifFilename = filename;
1004 gifRecordTime = seconds;
1005 if (cameraConfig.getGifPreroll() > 0) {
1006 snapCount = seconds;
1008 setupFfmpegFormat(FFmpegFormat.GIF);
1010 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1013 public String returnValueFromString(String rawString, String searchedString) {
1015 int index = rawString.indexOf(searchedString);
1016 if (index != -1) // -1 means "not found"
1018 result = rawString.substring(index + searchedString.length(), rawString.length());
1019 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1021 return result; // Did not find a carriage return.
1023 return result.substring(0, index);
1026 return ""; // Did not find the String we were searching for
1029 private void sendPTZRequest() {
1030 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1034 public void channelLinked(ChannelUID channelUID) {
1035 switch (channelUID.getId()) {
1036 case CHANNEL_MJPEG_URL:
1037 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1038 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
1040 case CHANNEL_HLS_URL:
1041 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1042 + getThing().getUID().getId() + "/ipcamera.m3u8"));
1044 case CHANNEL_IMAGE_URL:
1045 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1046 + getThing().getUID().getId() + "/ipcamera.jpg"));
1052 public void handleCommand(ChannelUID channelUID, Command command) {
1053 if (command instanceof RefreshType) {
1054 switch (channelUID.getId()) {
1056 if (onvifCamera.supportsPTZ()) {
1057 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1061 if (onvifCamera.supportsPTZ()) {
1062 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1066 if (onvifCamera.supportsPTZ()) {
1067 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1070 case CHANNEL_GOTO_PRESET:
1071 if (onvifCamera.supportsPTZ()) {
1072 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1076 } // caution "REFRESH" can still progress to brand Handlers below the else.
1078 switch (channelUID.getId()) {
1079 case CHANNEL_MP4_HISTORY_LENGTH:
1080 if (DecimalType.ZERO.equals(command)) {
1081 mp4HistoryLength = 0;
1083 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1086 case CHANNEL_GIF_HISTORY_LENGTH:
1087 if (DecimalType.ZERO.equals(command)) {
1088 gifHistoryLength = 0;
1090 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1093 case CHANNEL_FFMPEG_MOTION_CONTROL:
1094 if (OnOffType.ON.equals(command)) {
1095 motionAlarmEnabled = true;
1096 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1097 motionAlarmEnabled = false;
1098 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1099 } else if (command instanceof PercentType) {
1100 motionAlarmEnabled = true;
1101 motionThreshold = ((PercentType) command).toBigDecimal();
1103 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1105 case CHANNEL_START_STREAM:
1107 if (OnOffType.ON.equals(command)) {
1108 localHLS = ffmpegHLS;
1109 if (localHLS == null) {
1110 setupFfmpegFormat(FFmpegFormat.HLS);
1111 localHLS = ffmpegHLS;
1113 if (localHLS != null) {
1114 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1115 localHLS.startConverting();
1118 localHLS = ffmpegHLS;
1119 if (localHLS != null) {
1120 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1121 localHLS.setKeepAlive(1);
1125 case CHANNEL_EXTERNAL_MOTION:
1126 if (OnOffType.ON.equals(command)) {
1127 motionDetected(CHANNEL_EXTERNAL_MOTION);
1129 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1132 case CHANNEL_GOTO_PRESET:
1133 if (onvifCamera.supportsPTZ()) {
1134 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1137 case CHANNEL_POLL_IMAGE:
1138 if (OnOffType.ON.equals(command)) {
1139 if (snapshotUri.isEmpty()) {
1140 ffmpegSnapshotGeneration = true;
1141 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1142 updateImageChannel = false;
1144 updateImageChannel = true;
1145 updateSnapshot();// Allows this to change Image FPS on demand
1148 Ffmpeg localSnaps = ffmpegSnapshot;
1149 if (localSnaps != null) {
1150 localSnaps.stopConverting();
1151 ffmpegSnapshotGeneration = false;
1153 updateImageChannel = false;
1157 if (onvifCamera.supportsPTZ()) {
1158 if (command instanceof IncreaseDecreaseType) {
1159 if (command == IncreaseDecreaseType.INCREASE) {
1160 if (cameraConfig.getPtzContinuous()) {
1161 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1163 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1166 if (cameraConfig.getPtzContinuous()) {
1167 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1169 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1173 } else if (OnOffType.OFF.equals(command)) {
1174 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1177 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1178 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1182 if (onvifCamera.supportsPTZ()) {
1183 if (command instanceof IncreaseDecreaseType) {
1184 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1185 if (cameraConfig.getPtzContinuous()) {
1186 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1188 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1191 if (cameraConfig.getPtzContinuous()) {
1192 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1194 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1198 } else if (OnOffType.OFF.equals(command)) {
1199 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1202 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1203 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1207 if (onvifCamera.supportsPTZ()) {
1208 if (command instanceof IncreaseDecreaseType) {
1209 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1210 if (cameraConfig.getPtzContinuous()) {
1211 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1213 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1216 if (cameraConfig.getPtzContinuous()) {
1217 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1219 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1223 } else if (OnOffType.OFF.equals(command)) {
1224 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1227 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1228 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1233 // commands and refresh now get passed to brand handlers
1234 switch (thing.getThingTypeUID().getId()) {
1236 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1237 amcrestHandler.handleCommand(channelUID, command);
1238 if (lowPriorityRequests.isEmpty()) {
1239 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1243 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1244 dahuaHandler.handleCommand(channelUID, command);
1245 if (lowPriorityRequests.isEmpty()) {
1246 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1249 case DOORBIRD_THING:
1250 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1251 doorBirdHandler.handleCommand(channelUID, command);
1252 if (lowPriorityRequests.isEmpty()) {
1253 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1256 case HIKVISION_THING:
1257 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1258 hikvisionHandler.handleCommand(channelUID, command);
1259 if (lowPriorityRequests.isEmpty()) {
1260 lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1264 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1265 cameraConfig.getPassword());
1266 foscamHandler.handleCommand(channelUID, command);
1267 if (lowPriorityRequests.isEmpty()) {
1268 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1272 InstarHandler instarHandler = new InstarHandler(getHandle());
1273 instarHandler.handleCommand(channelUID, command);
1274 if (lowPriorityRequests.isEmpty()) {
1275 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1279 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1280 defaultHandler.handleCommand(channelUID, command);
1281 if (lowPriorityRequests.isEmpty()) {
1282 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1288 public void setChannelState(String channelToUpdate, State valueOf) {
1289 updateState(channelToUpdate, valueOf);
1292 private void bringCameraOnline() {
1294 updateStatus(ThingStatus.ONLINE);
1295 groupTracker.listOfOnlineCameraHandlers.add(this);
1296 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1297 Future<?> localFuture = cameraConnectionJob;
1298 if (localFuture != null) {
1299 localFuture.cancel(false);
1300 cameraConnectionJob = null;
1302 if (!snapshotUri.isEmpty()) {
1303 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1304 snapshotPolling = true;
1305 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000,
1306 cameraConfig.getPollTime(), TimeUnit.MILLISECONDS);
1310 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1312 if (!rtspUri.isEmpty()) {
1313 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1315 if (updateImageChannel) {
1316 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1318 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1320 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1321 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1322 handle.cameraOnline(getThing().getUID().getId());
1327 void snapshotIsFfmpeg() {
1328 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1330 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1331 bringCameraOnline();
1332 if (!rtspUri.isEmpty()) {
1333 updateImageChannel = false;
1334 ffmpegSnapshotGeneration = true;
1335 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1336 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1338 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1342 void pollingCameraConnection() {
1343 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1344 if (rtspUri.isEmpty()) {
1345 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1347 if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1354 if (!onvifCamera.isConnected()) {
1355 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1356 cameraConfig.getOnvifPort());
1357 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1359 if ("ffmpeg".equals(snapshotUri)) {
1361 } else if (!snapshotUri.isEmpty()) {
1363 } else if (!rtspUri.isEmpty()) {
1366 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1367 "Camera failed to report a valid Snaphot and/or RTSP URL. See readme on how to use the SNAPSHOT_URL_OVERRIDE feature.");
1371 public void cameraConfigError(String reason) {
1372 // wont try to reconnect again due to a config error being the cause.
1373 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1377 public void cameraCommunicationError(String reason) {
1378 // will try to reconnect again as camera may be rebooting.
1379 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1380 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1381 resetAndRetryConnecting();
1385 boolean streamIsStopped(String url) {
1386 ChannelTracking channelTracking = channelTrackingMap.get(url);
1387 if (channelTracking != null) {
1388 if (channelTracking.getChannel().isActive()) {
1389 return false; // stream is running.
1392 return true; // Stream stopped or never started.
1395 void snapshotRunnable() {
1396 // Snapshot should be first to keep consistent time between shots
1398 if (snapCount > 0) {
1399 if (--snapCount == 0) {
1400 setupFfmpegFormat(FFmpegFormat.GIF);
1405 private void takeSnapshot() {
1406 sendHttpGET(snapshotUri);
1409 private void updateSnapshot() {
1410 lastSnapshotRequest = Instant.now();
1411 mainEventLoopGroup.schedule(this::takeSnapshot, 0, TimeUnit.MILLISECONDS);
1414 public byte[] getSnapshot() {
1416 // Single gray pixel JPG to keep streams open when the camera goes offline so they dont stop.
1417 return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
1418 0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43,
1419 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
1420 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09,
1421 0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d,
1422 0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10,
1423 0x10, (byte) 0xff, (byte) 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
1424 (byte) 0xff, (byte) 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, (byte) 0xff, (byte) 0xda, 0x00, 0x08,
1425 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, (byte) 0xd2, (byte) 0xcf, 0x20, (byte) 0xff, (byte) 0xd9 };
1427 // Most cameras will return a 503 busy error if snapshot is faster than 1 second
1428 long lastUpdatedMs = Duration.between(lastSnapshotRequest, Instant.now()).toMillis();
1429 if (!snapshotPolling && !ffmpegSnapshotGeneration && lastUpdatedMs >= cameraConfig.getPollTime()) {
1432 lockCurrentSnapshot.lock();
1434 return currentSnapshot;
1436 lockCurrentSnapshot.unlock();
1440 public void stopSnapshotPolling() {
1441 Future<?> localFuture;
1442 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1443 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1444 snapshotPolling = false;
1445 localFuture = snapshotJob;
1446 if (localFuture != null) {
1447 localFuture.cancel(true);
1449 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1450 snapshotPolling = false;
1451 localFuture = snapshotJob;
1452 if (localFuture != null) {
1453 localFuture.cancel(true);
1458 public void startSnapshotPolling() {
1459 if (snapshotPolling || ffmpegSnapshotGeneration) {
1460 return; // Already polling or creating with FFmpeg from RTSP
1462 if (streamingSnapshotMjpeg || streamingAutoFps || cameraConfig.getUpdateImageWhen().contains("4")) {
1463 snapshotPolling = true;
1464 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 0, cameraConfig.getPollTime(),
1465 TimeUnit.MILLISECONDS);
1470 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep alarm
1471 * streams open and more.
1474 void pollCameraRunnable() {
1475 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1476 if (!lowPriorityRequests.isEmpty()) {
1477 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1478 lowPriorityCounter = 0;
1480 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1482 // what needs to be done every poll//
1483 switch (thing.getThingTypeUID().getId()) {
1485 if (!snapshotUri.isEmpty() && !snapshotPolling) {
1486 checkCameraConnection();
1488 // RTSP stream has stopped and we need it for snapshots
1489 if (ffmpegSnapshotGeneration) {
1490 Ffmpeg localSnapshot = ffmpegSnapshot;
1491 if (localSnapshot != null && !localSnapshot.getIsAlive()) {
1492 localSnapshot.startConverting();
1497 if (!snapshotPolling) {
1498 checkCameraConnection();
1500 if (!onvifCamera.isConnected()) {
1501 onvifCamera.connect(true);
1505 if (!snapshotPolling) {
1506 checkCameraConnection();
1508 noMotionDetected(CHANNEL_MOTION_ALARM);
1509 noMotionDetected(CHANNEL_PIR_ALARM);
1512 case HIKVISION_THING:
1513 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1514 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1515 cameraConfig.getIp());
1516 sendHttpGET("/ISAPI/Event/notification/alertStream");
1520 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1521 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1524 if (!snapshotPolling) {
1525 checkCameraConnection();
1527 // Check for alarms, channel for NVRs appears not to work at filtering.
1528 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1529 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1530 cameraConfig.getIp());
1531 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1534 case DOORBIRD_THING:
1535 if (!snapshotPolling) {
1536 checkCameraConnection();
1538 // Check for alarms, channel for NVRs appears not to work at filtering.
1539 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1540 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1541 cameraConfig.getIp());
1542 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1546 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1547 + cameraConfig.getPassword());
1550 Ffmpeg localHLS = ffmpegHLS;
1551 if (localHLS != null) {
1552 localHLS.checkKeepAlive();
1554 if (openChannels.size() > 10) {
1555 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1561 public void initialize() {
1562 cameraConfig = getConfigAs(CameraConfig.class);
1563 threadPool = Executors.newScheduledThreadPool(2);
1564 mainEventLoopGroup = new NioEventLoopGroup(3);
1565 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1566 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1567 rtspUri = cameraConfig.getFfmpegInput();
1568 if (cameraConfig.getFfmpegOutput().isEmpty()) {
1570 .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1572 // Known cameras will connect quicker if we skip ONVIF questions.
1573 switch (thing.getThingTypeUID().getId()) {
1576 if (mjpegUri.isEmpty()) {
1577 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1579 if (snapshotUri.isEmpty()) {
1580 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1583 case DOORBIRD_THING:
1584 if (mjpegUri.isEmpty()) {
1585 mjpegUri = "/bha-api/video.cgi";
1587 if (snapshotUri.isEmpty()) {
1588 snapshotUri = "/bha-api/image.cgi";
1592 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1593 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1594 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1595 if (mjpegUri.isEmpty()) {
1596 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1597 + cameraConfig.getPassword();
1599 if (snapshotUri.isEmpty()) {
1600 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1601 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1604 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1605 if (mjpegUri.isEmpty()) {
1606 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1608 if (snapshotUri.isEmpty()) {
1609 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1613 if (snapshotUri.isEmpty()) {
1614 snapshotUri = "/tmpfs/snap.jpg";
1616 if (mjpegUri.isEmpty()) {
1617 mjpegUri = "/mjpegstream.cgi?-chn=12";
1620 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
1621 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1622 + getThing().getUID().getId()
1623 + "/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");
1626 // for poll times 9 seconds and above don't display a warning about the Image channel.
1627 if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1629 "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.");
1631 // ONVIF and Instar event handling need the server started before connecting.
1632 startStreamServer();
1636 private void tryConnecting() {
1637 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1638 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1639 cameraConfig.getUser(), cameraConfig.getPassword());
1640 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1641 // Only use ONVIF events if it is not an API camera.
1642 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1644 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 30, TimeUnit.SECONDS);
1647 // What the camera needs to re-connect if the initialize() is not called.
1648 private void resetAndRetryConnecting() {
1653 private void offline() {
1655 snapshotPolling = false;
1656 Future<?> localFuture = pollCameraJob;
1657 if (localFuture != null) {
1658 localFuture.cancel(true);
1661 localFuture = snapshotJob;
1662 if (localFuture != null) {
1663 localFuture.cancel(true);
1666 localFuture = cameraConnectionJob;
1667 if (localFuture != null) {
1668 localFuture.cancel(true);
1671 Ffmpeg localFfmpeg = ffmpegHLS;
1672 if (localFfmpeg != null) {
1673 localFfmpeg.stopConverting();
1676 localFfmpeg = ffmpegRecord;
1677 if (localFfmpeg != null) {
1678 localFfmpeg.stopConverting();
1679 ffmpegRecord = null;
1681 localFfmpeg = ffmpegGIF;
1682 if (localFfmpeg != null) {
1683 localFfmpeg.stopConverting();
1686 localFfmpeg = ffmpegRtspHelper;
1687 if (localFfmpeg != null) {
1688 localFfmpeg.stopConverting();
1689 ffmpegRtspHelper = null;
1691 localFfmpeg = ffmpegMjpeg;
1692 if (localFfmpeg != null) {
1693 localFfmpeg.stopConverting();
1696 localFfmpeg = ffmpegSnapshot;
1697 if (localFfmpeg != null) {
1698 localFfmpeg.stopConverting();
1699 ffmpegSnapshot = null;
1701 onvifCamera.disconnect();
1702 openChannels.close();
1706 public void dispose() {
1708 CameraServlet localServlet = servlet;
1709 if (localServlet != null) {
1710 localServlet.dispose();
1711 localServlet = null;
1713 threadPool.shutdown();
1714 // inform all group handlers that this camera has gone offline
1715 groupTracker.listOfOnlineCameraHandlers.remove(this);
1716 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1717 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1718 handle.cameraOffline(this);
1720 basicAuth = ""; // clear out stored Password hash
1721 useDigestAuth = false;
1722 mainEventLoopGroup.shutdownGracefully();
1723 mainBootstrap = null;
1724 channelTrackingMap.clear();
1727 public String getWhiteList() {
1728 return cameraConfig.getIpWhitelist();
1732 public Collection<Class<? extends ThingHandlerService>> getServices() {
1733 return Collections.singleton(IpCameraActions.class);