2 * Copyright (c) 2010-2022 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 boundary = Helper.searchString(contentType, "boundary=");
236 if (mjpegUri.equals(requestUrl)) {
237 if (msg instanceof HttpMessage) {
238 // very start of stream only
239 mjpegContentType = contentType;
240 CameraServlet localServlet = servlet;
241 if (localServlet != null) {
242 logger.debug("Setting Content-Type to:{}", contentType);
243 localServlet.openStreams.updateContentType(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 if (mjpegUri.isEmpty() || "ffmpeg".equals(mjpegUri)) {
673 setupFfmpegFormat(FFmpegFormat.MJPEG);
676 closeChannel(getTinyUrl(mjpegUri));
677 // Dahua cameras crash if you refresh (close and open) the stream without this delay.
678 mainEventLoopGroup.schedule(this::openMjpegStream, 300, TimeUnit.MILLISECONDS);
681 private void openMjpegStream() {
682 sendHttpGET(mjpegUri);
685 private void openChannel(Channel channel, String httpRequestURL) {
686 ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
687 if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
688 tracker.setChannel(channel);
691 channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
694 public void closeChannel(String url) {
695 ChannelTracking channelTracking = channelTrackingMap.get(url);
696 if (channelTracking != null) {
697 if (channelTracking.getChannel().isOpen()) {
698 channelTracking.getChannel().close();
705 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
706 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
709 @SuppressWarnings("PMD.CompareObjectsWithEquals")
710 private void cleanChannels() {
711 for (Channel channel : openChannels) {
712 boolean oldChannel = true;
713 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
714 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
715 channelTrackingMap.remove(channelTracking.getRequestUrl());
717 if (channelTracking.getChannel() == channel) {
718 logger.debug("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
728 public void storeHttpReply(String url, String content) {
729 ChannelTracking channelTracking = channelTrackingMap.get(url);
730 if (channelTracking != null) {
731 channelTracking.setReply(content);
735 private void storeSnapshots() {
737 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
738 lockCurrentSnapshot.lock();
740 for (byte[] foo : fifoSnapshotBuffer) {
741 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
744 OutputStream fos = new FileOutputStream(file);
747 } catch (FileNotFoundException e) {
748 logger.warn("FileNotFoundException {}", e.getMessage());
749 } catch (IOException e) {
750 logger.warn("IOException {}", e.getMessage());
754 lockCurrentSnapshot.unlock();
758 public void setupFfmpegFormat(FFmpegFormat format) {
759 String inputOptions = cameraConfig.getFfmpegInputOptions();
760 if (cameraConfig.getFfmpegOutput().isEmpty()) {
761 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
764 if (rtspUri.isEmpty()) {
765 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
768 if (cameraConfig.getFfmpegLocation().isEmpty()) {
769 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
772 if (rtspUri.toLowerCase().contains("rtsp")) {
773 if (inputOptions.isEmpty()) {
774 inputOptions = "-rtsp_transport tcp";
778 // Make sure the folder exists, if not create it.
779 new File(cameraConfig.getFfmpegOutput()).mkdirs();
782 if (ffmpegHLS == null) {
783 if (!inputOptions.isEmpty()) {
784 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
785 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
786 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
787 cameraConfig.getUser(), cameraConfig.getPassword());
789 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
790 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
791 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
792 cameraConfig.getPassword());
795 Ffmpeg localHLS = ffmpegHLS;
796 if (localHLS != null) {
797 localHLS.startConverting();
801 if (cameraConfig.getGifPreroll() > 0) {
802 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
803 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
804 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
805 + cameraConfig.getGifOutOptions(),
806 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
807 cameraConfig.getPassword());
809 if (!inputOptions.isEmpty()) {
810 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
812 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
814 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
815 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
816 cameraConfig.getUser(), cameraConfig.getPassword());
818 if (cameraConfig.getGifPreroll() > 0) {
821 Ffmpeg localGIF = ffmpegGIF;
822 if (localGIF != null) {
823 localGIF.startConverting();
824 if (gifHistory.isEmpty()) {
825 gifHistory = gifFilename;
826 } else if (!"ipcamera".equals(gifFilename)) {
827 gifHistory = gifFilename + "," + gifHistory;
828 if (gifHistoryLength > 49) {
829 int endIndex = gifHistory.lastIndexOf(",");
830 gifHistory = gifHistory.substring(0, endIndex);
833 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
837 if (!inputOptions.isEmpty()) {
838 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
840 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
842 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
843 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
844 cameraConfig.getUser(), cameraConfig.getPassword());
845 Ffmpeg localRecord = ffmpegRecord;
846 if (localRecord != null) {
847 localRecord.startConverting();
848 if (mp4History.isEmpty()) {
849 mp4History = mp4Filename;
850 } else if (!"ipcamera".equals(mp4Filename)) {
851 mp4History = mp4Filename + "," + mp4History;
852 if (mp4HistoryLength > 49) {
853 int endIndex = mp4History.lastIndexOf(",");
854 mp4History = mp4History.substring(0, endIndex);
858 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
861 Ffmpeg localAlarms = ffmpegRtspHelper;
862 if (localAlarms != null) {
863 localAlarms.stopConverting();
864 if (!audioAlarmEnabled && !motionAlarmEnabled) {
868 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
869 String filterOptions = "";
870 if (!audioAlarmEnabled) {
871 filterOptions = "-an";
873 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
875 if (!motionAlarmEnabled && !ffmpegSnapshotGeneration) {
876 filterOptions = filterOptions.concat(" -vn");
877 } else if (motionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
878 String usersMotionOptions = cameraConfig.getMotionOptions();
879 if (usersMotionOptions.startsWith("-")) {
880 // Need to put the users custom options first in the chain before the motion is detected
881 filterOptions += " " + usersMotionOptions + ",select='gte(scene,"
882 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
884 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
885 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
887 } else if (motionAlarmEnabled) {
888 filterOptions = filterOptions.concat(" -vf select='gte(scene,"
889 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print");
891 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
892 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
893 localAlarms = ffmpegRtspHelper;
894 if (localAlarms != null) {
895 localAlarms.startConverting();
899 if (ffmpegMjpeg == null) {
900 if (inputOptions.isEmpty()) {
901 inputOptions = "-hide_banner -loglevel warning";
903 inputOptions += " -hide_banner -loglevel warning";
905 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
906 cameraConfig.getMjpegOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
907 + getThing().getUID().getId() + "/ipcamera.jpg",
908 cameraConfig.getUser(), cameraConfig.getPassword());
910 Ffmpeg localMjpeg = ffmpegMjpeg;
911 if (localMjpeg != null) {
912 localMjpeg.startConverting();
916 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
917 if (ffmpegSnapshot == null) {
918 if (inputOptions.isEmpty()) {
920 inputOptions = "-threads 1 -skip_frame nokey -hide_banner -loglevel warning";
922 inputOptions += " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
924 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
925 cameraConfig.getSnapshotOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
926 + getThing().getUID().getId() + "/snapshot.jpg",
927 cameraConfig.getUser(), cameraConfig.getPassword());
929 Ffmpeg localSnaps = ffmpegSnapshot;
930 if (localSnaps != null) {
931 localSnaps.startConverting();
937 public void noMotionDetected(String thisAlarmsChannel) {
938 setChannelState(thisAlarmsChannel, OnOffType.OFF);
939 firstMotionAlarm = false;
940 motionAlarmUpdateSnapshot = false;
941 motionDetected = false;
942 if (streamingAutoFps) {
943 stopSnapshotPolling();
944 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
945 stopSnapshotPolling();
950 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
951 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
952 * tampering with the camera.
954 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
955 updateState(thisAlarmsChannel, state);
958 public void motionDetected(String thisAlarmsChannel) {
959 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
960 updateState(thisAlarmsChannel, OnOffType.ON);
961 motionDetected = true;
962 if (streamingAutoFps) {
963 startSnapshotPolling();
965 if (cameraConfig.getUpdateImageWhen().contains("2")) {
966 if (!firstMotionAlarm) {
967 if (!snapshotUri.isEmpty()) {
970 firstMotionAlarm = true;// reset back to false when the jpg arrives.
972 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
973 if (!snapshotPolling) {
974 startSnapshotPolling();
976 firstMotionAlarm = true;
977 motionAlarmUpdateSnapshot = true;
981 public void audioDetected() {
982 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
983 if (cameraConfig.getUpdateImageWhen().contains("3")) {
984 if (!firstAudioAlarm) {
985 if (!snapshotUri.isEmpty()) {
988 firstAudioAlarm = true;// reset back to false when the jpg arrives.
990 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
991 firstAudioAlarm = true;
992 audioAlarmUpdateSnapshot = true;
996 public void noAudioDetected() {
997 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
998 firstAudioAlarm = false;
999 audioAlarmUpdateSnapshot = false;
1002 public void recordMp4(String filename, int seconds) {
1003 mp4Filename = filename;
1004 mp4RecordTime = seconds;
1005 setupFfmpegFormat(FFmpegFormat.RECORD);
1006 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1009 public void recordGif(String filename, int seconds) {
1010 gifFilename = filename;
1011 gifRecordTime = seconds;
1012 if (cameraConfig.getGifPreroll() > 0) {
1013 snapCount = seconds;
1015 setupFfmpegFormat(FFmpegFormat.GIF);
1017 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1020 public String returnValueFromString(String rawString, String searchedString) {
1022 int index = rawString.indexOf(searchedString);
1023 if (index != -1) // -1 means "not found"
1025 result = rawString.substring(index + searchedString.length(), rawString.length());
1026 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1028 return result; // Did not find a carriage return.
1030 return result.substring(0, index);
1033 return ""; // Did not find the String we were searching for
1036 private void sendPTZRequest() {
1037 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1041 public void channelLinked(ChannelUID channelUID) {
1042 switch (channelUID.getId()) {
1043 case CHANNEL_MJPEG_URL:
1044 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1045 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
1047 case CHANNEL_HLS_URL:
1048 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1049 + getThing().getUID().getId() + "/ipcamera.m3u8"));
1051 case CHANNEL_IMAGE_URL:
1052 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1053 + getThing().getUID().getId() + "/ipcamera.jpg"));
1059 public void handleCommand(ChannelUID channelUID, Command command) {
1060 if (command instanceof RefreshType) {
1061 switch (channelUID.getId()) {
1063 if (onvifCamera.supportsPTZ()) {
1064 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1068 if (onvifCamera.supportsPTZ()) {
1069 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1073 if (onvifCamera.supportsPTZ()) {
1074 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1077 case CHANNEL_GOTO_PRESET:
1078 if (onvifCamera.supportsPTZ()) {
1079 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1083 } // caution "REFRESH" can still progress to brand Handlers below the else.
1085 switch (channelUID.getId()) {
1086 case CHANNEL_MP4_HISTORY_LENGTH:
1087 if (DecimalType.ZERO.equals(command)) {
1088 mp4HistoryLength = 0;
1090 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1093 case CHANNEL_GIF_HISTORY_LENGTH:
1094 if (DecimalType.ZERO.equals(command)) {
1095 gifHistoryLength = 0;
1097 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1100 case CHANNEL_FFMPEG_MOTION_CONTROL:
1101 if (OnOffType.ON.equals(command)) {
1102 motionAlarmEnabled = true;
1103 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1104 motionAlarmEnabled = false;
1105 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1106 } else if (command instanceof PercentType) {
1107 motionAlarmEnabled = true;
1108 motionThreshold = ((PercentType) command).toBigDecimal();
1110 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1112 case CHANNEL_START_STREAM:
1114 if (OnOffType.ON.equals(command)) {
1115 localHLS = ffmpegHLS;
1116 if (localHLS == null) {
1117 setupFfmpegFormat(FFmpegFormat.HLS);
1118 localHLS = ffmpegHLS;
1120 if (localHLS != null) {
1121 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1122 localHLS.startConverting();
1125 localHLS = ffmpegHLS;
1126 if (localHLS != null) {
1127 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1128 localHLS.setKeepAlive(1);
1132 case CHANNEL_EXTERNAL_MOTION:
1133 if (OnOffType.ON.equals(command)) {
1134 motionDetected(CHANNEL_EXTERNAL_MOTION);
1136 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1139 case CHANNEL_GOTO_PRESET:
1140 if (onvifCamera.supportsPTZ()) {
1141 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1144 case CHANNEL_POLL_IMAGE:
1145 if (OnOffType.ON.equals(command)) {
1146 if (snapshotUri.isEmpty()) {
1147 ffmpegSnapshotGeneration = true;
1148 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1149 updateImageChannel = false;
1151 updateImageChannel = true;
1152 updateSnapshot();// Allows this to change Image FPS on demand
1155 Ffmpeg localSnaps = ffmpegSnapshot;
1156 if (localSnaps != null) {
1157 localSnaps.stopConverting();
1158 ffmpegSnapshotGeneration = false;
1160 updateImageChannel = false;
1164 if (onvifCamera.supportsPTZ()) {
1165 if (command instanceof IncreaseDecreaseType) {
1166 if (command == IncreaseDecreaseType.INCREASE) {
1167 if (cameraConfig.getPtzContinuous()) {
1168 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1170 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1173 if (cameraConfig.getPtzContinuous()) {
1174 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1176 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1180 } else if (OnOffType.OFF.equals(command)) {
1181 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1184 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1185 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1189 if (onvifCamera.supportsPTZ()) {
1190 if (command instanceof IncreaseDecreaseType) {
1191 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1192 if (cameraConfig.getPtzContinuous()) {
1193 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1195 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1198 if (cameraConfig.getPtzContinuous()) {
1199 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1201 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1205 } else if (OnOffType.OFF.equals(command)) {
1206 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1209 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1210 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1214 if (onvifCamera.supportsPTZ()) {
1215 if (command instanceof IncreaseDecreaseType) {
1216 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1217 if (cameraConfig.getPtzContinuous()) {
1218 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1220 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1223 if (cameraConfig.getPtzContinuous()) {
1224 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1226 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1230 } else if (OnOffType.OFF.equals(command)) {
1231 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1234 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1235 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1240 // commands and refresh now get passed to brand handlers
1241 switch (thing.getThingTypeUID().getId()) {
1243 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1244 amcrestHandler.handleCommand(channelUID, command);
1245 if (lowPriorityRequests.isEmpty()) {
1246 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1250 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1251 dahuaHandler.handleCommand(channelUID, command);
1252 if (lowPriorityRequests.isEmpty()) {
1253 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1256 case DOORBIRD_THING:
1257 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1258 doorBirdHandler.handleCommand(channelUID, command);
1259 if (lowPriorityRequests.isEmpty()) {
1260 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1263 case HIKVISION_THING:
1264 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1265 hikvisionHandler.handleCommand(channelUID, command);
1266 if (lowPriorityRequests.isEmpty()) {
1267 lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1271 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1272 cameraConfig.getPassword());
1273 foscamHandler.handleCommand(channelUID, command);
1274 if (lowPriorityRequests.isEmpty()) {
1275 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1279 InstarHandler instarHandler = new InstarHandler(getHandle());
1280 instarHandler.handleCommand(channelUID, command);
1281 if (lowPriorityRequests.isEmpty()) {
1282 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1286 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1287 defaultHandler.handleCommand(channelUID, command);
1288 if (lowPriorityRequests.isEmpty()) {
1289 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1295 public void setChannelState(String channelToUpdate, State valueOf) {
1296 updateState(channelToUpdate, valueOf);
1299 private void bringCameraOnline() {
1301 updateStatus(ThingStatus.ONLINE);
1302 groupTracker.listOfOnlineCameraHandlers.add(this);
1303 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1304 Future<?> localFuture = cameraConnectionJob;
1305 if (localFuture != null) {
1306 localFuture.cancel(false);
1307 cameraConnectionJob = null;
1309 if (!snapshotUri.isEmpty()) {
1310 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1311 snapshotPolling = true;
1312 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000,
1313 cameraConfig.getPollTime(), TimeUnit.MILLISECONDS);
1317 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1319 // auto restart mjpeg stream now camera is back online.
1320 CameraServlet localServlet = servlet;
1321 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1322 openCamerasStream();
1325 if (!rtspUri.isEmpty()) {
1326 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1328 if (updateImageChannel) {
1329 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1331 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1333 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1334 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1335 handle.cameraOnline(getThing().getUID().getId());
1340 void snapshotIsFfmpeg() {
1341 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1343 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1344 bringCameraOnline();
1345 if (!rtspUri.isEmpty()) {
1346 updateImageChannel = false;
1347 ffmpegSnapshotGeneration = true;
1348 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1349 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1351 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1355 void pollingCameraConnection() {
1357 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1358 if (rtspUri.isEmpty()) {
1359 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1361 if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1368 if (!onvifCamera.isConnected()) {
1369 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1370 cameraConfig.getOnvifPort());
1371 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1373 if ("ffmpeg".equals(snapshotUri)) {
1375 } else if (!snapshotUri.isEmpty()) {
1377 } else if (!rtspUri.isEmpty()) {
1380 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1381 "Camera failed to report a valid Snaphot and/or RTSP URL. Check user/pass is correct, or use the advanced configs to manually provide a URL.");
1385 public void cameraConfigError(String reason) {
1386 // wont try to reconnect again due to a config error being the cause.
1387 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1391 public void cameraCommunicationError(String reason) {
1392 // will try to reconnect again as camera may be rebooting.
1393 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1394 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1395 resetAndRetryConnecting();
1399 boolean streamIsStopped(String url) {
1400 ChannelTracking channelTracking = channelTrackingMap.get(url);
1401 if (channelTracking != null) {
1402 if (channelTracking.getChannel().isActive()) {
1403 return false; // stream is running.
1406 return true; // Stream stopped or never started.
1409 void snapshotRunnable() {
1410 // Snapshot should be first to keep consistent time between shots
1412 if (snapCount > 0) {
1413 if (--snapCount == 0) {
1414 setupFfmpegFormat(FFmpegFormat.GIF);
1419 private void takeSnapshot() {
1420 sendHttpGET(snapshotUri);
1423 private void updateSnapshot() {
1424 lastSnapshotRequest = Instant.now();
1425 mainEventLoopGroup.schedule(this::takeSnapshot, 0, TimeUnit.MILLISECONDS);
1428 public byte[] getSnapshot() {
1430 // Single gray pixel JPG to keep streams open when the camera goes offline so they dont stop.
1431 return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
1432 0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43,
1433 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
1434 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09,
1435 0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d,
1436 0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10,
1437 0x10, (byte) 0xff, (byte) 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
1438 (byte) 0xff, (byte) 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, (byte) 0xff, (byte) 0xda, 0x00, 0x08,
1439 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, (byte) 0xd2, (byte) 0xcf, 0x20, (byte) 0xff, (byte) 0xd9 };
1441 // Most cameras will return a 503 busy error if snapshot is faster than 1 second
1442 long lastUpdatedMs = Duration.between(lastSnapshotRequest, Instant.now()).toMillis();
1443 if (!snapshotPolling && !ffmpegSnapshotGeneration && lastUpdatedMs >= cameraConfig.getPollTime()) {
1446 lockCurrentSnapshot.lock();
1448 return currentSnapshot;
1450 lockCurrentSnapshot.unlock();
1454 public void stopSnapshotPolling() {
1455 Future<?> localFuture;
1456 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1457 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1458 snapshotPolling = false;
1459 localFuture = snapshotJob;
1460 if (localFuture != null) {
1461 localFuture.cancel(true);
1463 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1464 snapshotPolling = false;
1465 localFuture = snapshotJob;
1466 if (localFuture != null) {
1467 localFuture.cancel(true);
1472 public void startSnapshotPolling() {
1473 if (snapshotPolling || ffmpegSnapshotGeneration) {
1474 return; // Already polling or creating with FFmpeg from RTSP
1476 if (streamingSnapshotMjpeg || streamingAutoFps || cameraConfig.getUpdateImageWhen().contains("4")) {
1477 snapshotPolling = true;
1478 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 0, cameraConfig.getPollTime(),
1479 TimeUnit.MILLISECONDS);
1484 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep alarm
1485 * streams open and more.
1488 void pollCameraRunnable() {
1489 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1490 if (!lowPriorityRequests.isEmpty()) {
1491 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1492 lowPriorityCounter = 0;
1494 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1496 // what needs to be done every poll//
1497 switch (thing.getThingTypeUID().getId()) {
1499 if (!snapshotUri.isEmpty() && !snapshotPolling) {
1500 checkCameraConnection();
1502 // RTSP stream has stopped and we need it for snapshots
1503 if (ffmpegSnapshotGeneration) {
1504 Ffmpeg localSnapshot = ffmpegSnapshot;
1505 if (localSnapshot != null && !localSnapshot.getIsAlive()) {
1506 localSnapshot.startConverting();
1511 if (!snapshotPolling) {
1512 checkCameraConnection();
1514 if (!onvifCamera.isConnected()) {
1515 onvifCamera.connect(true);
1519 if (!snapshotPolling) {
1520 checkCameraConnection();
1522 noMotionDetected(CHANNEL_MOTION_ALARM);
1523 noMotionDetected(CHANNEL_PIR_ALARM);
1526 case HIKVISION_THING:
1527 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1528 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1529 cameraConfig.getIp());
1530 sendHttpGET("/ISAPI/Event/notification/alertStream");
1534 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1535 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1538 if (!snapshotPolling) {
1539 checkCameraConnection();
1541 // Check for alarms, channel for NVRs appears not to work at filtering.
1542 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1543 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1544 cameraConfig.getIp());
1545 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1548 case DOORBIRD_THING:
1549 if (!snapshotPolling) {
1550 checkCameraConnection();
1552 // Check for alarms, channel for NVRs appears not to work at filtering.
1553 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1554 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1555 cameraConfig.getIp());
1556 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1560 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1561 + cameraConfig.getPassword());
1564 Ffmpeg localHLS = ffmpegHLS;
1565 if (localHLS != null) {
1566 localHLS.checkKeepAlive();
1568 if (openChannels.size() > 10) {
1569 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1575 public void initialize() {
1576 cameraConfig = getConfigAs(CameraConfig.class);
1577 threadPool = Executors.newScheduledThreadPool(2);
1578 mainEventLoopGroup = new NioEventLoopGroup(3);
1579 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1580 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1581 rtspUri = cameraConfig.getFfmpegInput();
1582 if (cameraConfig.getFfmpegOutput().isEmpty()) {
1584 .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1586 // Known cameras will connect quicker if we skip ONVIF questions.
1587 switch (thing.getThingTypeUID().getId()) {
1590 if (mjpegUri.isEmpty()) {
1591 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1593 if (snapshotUri.isEmpty()) {
1594 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1597 case DOORBIRD_THING:
1598 if (mjpegUri.isEmpty()) {
1599 mjpegUri = "/bha-api/video.cgi";
1601 if (snapshotUri.isEmpty()) {
1602 snapshotUri = "/bha-api/image.cgi";
1606 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1607 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1608 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1609 if (mjpegUri.isEmpty()) {
1610 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1611 + cameraConfig.getPassword();
1613 if (snapshotUri.isEmpty()) {
1614 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1615 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1618 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1619 if (mjpegUri.isEmpty()) {
1620 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1622 if (snapshotUri.isEmpty()) {
1623 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1627 if (snapshotUri.isEmpty()) {
1628 snapshotUri = "/tmpfs/snap.jpg";
1630 if (mjpegUri.isEmpty()) {
1631 mjpegUri = "/mjpegstream.cgi?-chn=12";
1634 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
1635 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1636 + getThing().getUID().getId()
1637 + "/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");
1640 // for poll times 9 seconds and above don't display a warning about the Image channel.
1641 if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1643 "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.");
1645 // ONVIF and Instar event handling need the server started before connecting.
1646 startStreamServer();
1650 private void tryConnecting() {
1651 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1652 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1653 cameraConfig.getUser(), cameraConfig.getPassword());
1654 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1655 // Only use ONVIF events if it is not an API camera.
1656 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1658 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 8, TimeUnit.SECONDS);
1661 private void keepMjpegRunning() {
1662 CameraServlet localServlet = servlet;
1663 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1664 if (!mjpegUri.isEmpty() && !"ffmpeg".equals(mjpegUri)) {
1665 localServlet.openStreams.queueFrame(("--" + localServlet.openStreams.boundary + "\r\n\r\n").getBytes());
1667 localServlet.openStreams.queueFrame(getSnapshot());
1671 // What the camera needs to re-connect if the initialize() is not called.
1672 private void resetAndRetryConnecting() {
1677 private void offline() {
1679 snapshotPolling = false;
1680 Future<?> localFuture = pollCameraJob;
1681 if (localFuture != null) {
1682 localFuture.cancel(true);
1685 localFuture = snapshotJob;
1686 if (localFuture != null) {
1687 localFuture.cancel(true);
1690 localFuture = cameraConnectionJob;
1691 if (localFuture != null) {
1692 localFuture.cancel(true);
1695 Ffmpeg localFfmpeg = ffmpegHLS;
1696 if (localFfmpeg != null) {
1697 localFfmpeg.stopConverting();
1700 localFfmpeg = ffmpegRecord;
1701 if (localFfmpeg != null) {
1702 localFfmpeg.stopConverting();
1703 ffmpegRecord = null;
1705 localFfmpeg = ffmpegGIF;
1706 if (localFfmpeg != null) {
1707 localFfmpeg.stopConverting();
1710 localFfmpeg = ffmpegRtspHelper;
1711 if (localFfmpeg != null) {
1712 localFfmpeg.stopConverting();
1713 ffmpegRtspHelper = null;
1715 localFfmpeg = ffmpegMjpeg;
1716 if (localFfmpeg != null) {
1717 localFfmpeg.stopConverting();
1720 localFfmpeg = ffmpegSnapshot;
1721 if (localFfmpeg != null) {
1722 localFfmpeg.stopConverting();
1723 ffmpegSnapshot = null;
1725 onvifCamera.disconnect();
1726 openChannels.close();
1730 public void dispose() {
1732 CameraServlet localServlet = servlet;
1733 if (localServlet != null) {
1734 localServlet.dispose();
1735 localServlet = null;
1737 threadPool.shutdown();
1738 // inform all group handlers that this camera has gone offline
1739 groupTracker.listOfOnlineCameraHandlers.remove(this);
1740 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1741 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1742 handle.cameraOffline(this);
1744 basicAuth = ""; // clear out stored Password hash
1745 useDigestAuth = false;
1746 mainEventLoopGroup.shutdownGracefully();
1747 mainBootstrap = null;
1748 channelTrackingMap.clear();
1751 public String getWhiteList() {
1752 return cameraConfig.getIpWhitelist();
1756 public Collection<Class<? extends ThingHandlerService>> getServices() {
1757 return Collections.singleton(IpCameraActions.class);