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.Instant;
27 import java.util.ArrayList;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.LinkedList;
31 import java.util.List;
33 import java.util.concurrent.ConcurrentHashMap;
34 import java.util.concurrent.Executors;
35 import java.util.concurrent.Future;
36 import java.util.concurrent.ScheduledExecutorService;
37 import java.util.concurrent.ScheduledFuture;
38 import java.util.concurrent.TimeUnit;
39 import java.util.concurrent.locks.ReentrantLock;
41 import org.eclipse.jdt.annotation.NonNullByDefault;
42 import org.eclipse.jdt.annotation.Nullable;
43 import org.openhab.binding.ipcamera.internal.AmcrestHandler;
44 import org.openhab.binding.ipcamera.internal.CameraConfig;
45 import org.openhab.binding.ipcamera.internal.ChannelTracking;
46 import org.openhab.binding.ipcamera.internal.DahuaHandler;
47 import org.openhab.binding.ipcamera.internal.DoorBirdHandler;
48 import org.openhab.binding.ipcamera.internal.Ffmpeg;
49 import org.openhab.binding.ipcamera.internal.FoscamHandler;
50 import org.openhab.binding.ipcamera.internal.GroupTracker;
51 import org.openhab.binding.ipcamera.internal.Helper;
52 import org.openhab.binding.ipcamera.internal.HikvisionHandler;
53 import org.openhab.binding.ipcamera.internal.HttpOnlyHandler;
54 import org.openhab.binding.ipcamera.internal.InstarHandler;
55 import org.openhab.binding.ipcamera.internal.IpCameraActions;
56 import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
57 import org.openhab.binding.ipcamera.internal.IpCameraDynamicStateDescriptionProvider;
58 import org.openhab.binding.ipcamera.internal.MyNettyAuthHandler;
59 import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection;
60 import org.openhab.binding.ipcamera.internal.servlet.CameraServlet;
61 import org.openhab.core.OpenHAB;
62 import org.openhab.core.library.types.DecimalType;
63 import org.openhab.core.library.types.IncreaseDecreaseType;
64 import org.openhab.core.library.types.OnOffType;
65 import org.openhab.core.library.types.PercentType;
66 import org.openhab.core.library.types.RawType;
67 import org.openhab.core.library.types.StringType;
68 import org.openhab.core.thing.ChannelUID;
69 import org.openhab.core.thing.Thing;
70 import org.openhab.core.thing.ThingStatus;
71 import org.openhab.core.thing.ThingStatusDetail;
72 import org.openhab.core.thing.binding.BaseThingHandler;
73 import org.openhab.core.thing.binding.ThingHandlerService;
74 import org.openhab.core.types.Command;
75 import org.openhab.core.types.RefreshType;
76 import org.openhab.core.types.State;
77 import org.osgi.service.http.HttpService;
78 import org.slf4j.Logger;
79 import org.slf4j.LoggerFactory;
81 import io.netty.bootstrap.Bootstrap;
82 import io.netty.buffer.ByteBuf;
83 import io.netty.buffer.Unpooled;
84 import io.netty.channel.Channel;
85 import io.netty.channel.ChannelDuplexHandler;
86 import io.netty.channel.ChannelFuture;
87 import io.netty.channel.ChannelFutureListener;
88 import io.netty.channel.ChannelHandlerContext;
89 import io.netty.channel.ChannelInitializer;
90 import io.netty.channel.ChannelOption;
91 import io.netty.channel.EventLoopGroup;
92 import io.netty.channel.group.ChannelGroup;
93 import io.netty.channel.group.DefaultChannelGroup;
94 import io.netty.channel.nio.NioEventLoopGroup;
95 import io.netty.channel.socket.SocketChannel;
96 import io.netty.channel.socket.nio.NioSocketChannel;
97 import io.netty.handler.codec.base64.Base64;
98 import io.netty.handler.codec.http.DefaultFullHttpRequest;
99 import io.netty.handler.codec.http.FullHttpRequest;
100 import io.netty.handler.codec.http.HttpClientCodec;
101 import io.netty.handler.codec.http.HttpContent;
102 import io.netty.handler.codec.http.HttpHeaderValues;
103 import io.netty.handler.codec.http.HttpMessage;
104 import io.netty.handler.codec.http.HttpMethod;
105 import io.netty.handler.codec.http.HttpResponse;
106 import io.netty.handler.codec.http.HttpVersion;
107 import io.netty.handler.codec.http.LastHttpContent;
108 import io.netty.handler.timeout.IdleState;
109 import io.netty.handler.timeout.IdleStateEvent;
110 import io.netty.handler.timeout.IdleStateHandler;
111 import io.netty.util.CharsetUtil;
112 import io.netty.util.ReferenceCountUtil;
113 import io.netty.util.concurrent.GlobalEventExecutor;
116 * The {@link IpCameraHandler} is responsible for handling commands, which are
117 * sent to one of the channels.
119 * @author Matthew Skinner - Initial contribution
123 public class IpCameraHandler extends BaseThingHandler {
124 public final Logger logger = LoggerFactory.getLogger(getClass());
125 public final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
126 private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(4);
127 private GroupTracker groupTracker;
128 public CameraConfig cameraConfig = new CameraConfig();
130 // ChannelGroup is thread safe
131 public final ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
132 private final HttpService httpService;
133 private @Nullable CameraServlet servlet;
134 public String mjpegContentType = "";
135 public @Nullable Ffmpeg ffmpegHLS = null;
136 public @Nullable Ffmpeg ffmpegRecord = null;
137 public @Nullable Ffmpeg ffmpegGIF = null;
138 public @Nullable Ffmpeg ffmpegRtspHelper = null;
139 public @Nullable Ffmpeg ffmpegMjpeg = null;
140 public @Nullable Ffmpeg ffmpegSnapshot = null;
141 public boolean streamingAutoFps = false;
142 public boolean motionDetected = false;
143 public Instant currentSnapshotTime = Instant.now();
144 private @Nullable ScheduledFuture<?> cameraConnectionJob = null;
145 private @Nullable ScheduledFuture<?> pollCameraJob = null;
146 private @Nullable ScheduledFuture<?> snapshotJob = null;
147 private @Nullable Bootstrap mainBootstrap;
148 private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup(1);
149 private FullHttpRequest putRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
151 private String gifFilename = "ipcamera";
152 private String gifHistory = "";
153 private String mp4History = "";
154 public int gifHistoryLength;
155 public int mp4HistoryLength;
156 private String mp4Filename = "ipcamera";
157 private int mp4RecordTime;
158 private int gifRecordTime = 5;
159 private LinkedList<byte[]> fifoSnapshotBuffer = new LinkedList<byte[]>();
160 private int snapCount;
161 private boolean updateImageChannel = false;
162 private byte lowPriorityCounter = 0;
163 public String hostIp;
164 public Map<String, ChannelTracking> channelTrackingMap = new ConcurrentHashMap<>();
165 public List<String> lowPriorityRequests = new ArrayList<>(0);
167 // basicAuth MUST remain private as it holds the cameraConfig.getPassword()
168 private String basicAuth = "";
169 public boolean useBasicAuth = false;
170 public boolean useDigestAuth = false;
171 public String snapshotUri = "";
172 public String mjpegUri = "";
173 private byte[] currentSnapshot = new byte[] { (byte) 0x00 };
174 public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
175 public String rtspUri = "";
176 public boolean audioAlarmUpdateSnapshot = false;
177 private boolean motionAlarmUpdateSnapshot = false;
178 private boolean isOnline = false; // Used so only 1 error is logged when a network issue occurs.
179 private boolean firstAudioAlarm = false;
180 private boolean firstMotionAlarm = false;
181 public BigDecimal motionThreshold = BigDecimal.ZERO;
182 public int audioThreshold = 35;
183 public boolean streamingSnapshotMjpeg = false;
184 public boolean motionAlarmEnabled = false;
185 public boolean audioAlarmEnabled = false;
186 public boolean ffmpegSnapshotGeneration = false;
187 public boolean snapshotPolling = false;
188 public OnvifConnection onvifCamera = new OnvifConnection(this, "", "", "");
190 // These methods handle the response from all camera brands, nothing specific to 1 brand.
191 private class CommonCameraHandler extends ChannelDuplexHandler {
192 private int bytesToRecieve = 0;
193 private int bytesAlreadyRecieved = 0;
194 private byte[] incomingJpeg = new byte[0];
195 private String incomingMessage = "";
196 private String contentType = "empty";
197 private String boundary = "";
198 private Object reply = new Object();
199 private String requestUrl = "";
200 private boolean closeConnection = true;
201 private boolean isChunked = false;
203 public void setURL(String url) {
208 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
209 if (msg == null || ctx == null) {
213 if (msg instanceof HttpResponse) {
214 HttpResponse response = (HttpResponse) msg;
215 if (response.status().code() != 401) {
216 if (!response.headers().isEmpty()) {
217 for (String name : response.headers().names()) {
218 // Some cameras use first letter uppercase and others dont.
219 switch (name.toLowerCase()) { // Possible localization issues doing this
221 contentType = response.headers().getAsString(name);
223 case "content-length":
224 bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
227 if (response.headers().getAsString(name).contains("keep-alive")) {
228 closeConnection = false;
231 case "transfer-encoding":
232 if (response.headers().getAsString(name).contains("chunked")) {
238 if (contentType.contains("multipart")) {
239 closeConnection = false;
240 if (mjpegUri.equals(requestUrl)) {
241 if (msg instanceof HttpMessage) {
242 // very start of stream only
243 mjpegContentType = contentType;
244 CameraServlet localServlet = servlet;
245 if (localServlet != null) {
246 localServlet.openStreams.updateContentType(contentType);
250 boundary = Helper.searchString(contentType, "boundary=");
252 } else if (contentType.contains("image/jp")) {
253 if (bytesToRecieve == 0) {
254 bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
255 logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
257 incomingJpeg = new byte[bytesToRecieve];
262 if (msg instanceof HttpContent) {
263 if (mjpegUri.equals(requestUrl)) {
264 // multiple MJPEG stream packets come back as this.
265 HttpContent content = (HttpContent) msg;
266 byte[] chunkedFrame = new byte[content.content().readableBytes()];
267 content.content().getBytes(content.content().readerIndex(), chunkedFrame);
268 CameraServlet localServlet = servlet;
269 if (localServlet != null) {
270 localServlet.openStreams.queueFrame(chunkedFrame);
273 HttpContent content = (HttpContent) msg;
274 // Found some cameras use Content-Type: image/jpg instead of image/jpeg
275 if (contentType.contains("image/jp")) {
276 for (int i = 0; i < content.content().capacity(); i++) {
277 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
279 if (content instanceof LastHttpContent) {
280 processSnapshot(incomingJpeg);
281 // testing next line and if works need to do a full cleanup of this function.
282 closeConnection = true;
283 if (closeConnection) {
287 bytesAlreadyRecieved = 0;
290 } else { // incomingMessage that is not an IMAGE
291 if (incomingMessage.isEmpty()) {
292 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
294 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
296 bytesAlreadyRecieved = incomingMessage.length();
297 if (content instanceof LastHttpContent) {
298 // If it is not an image send it on to the next handler//
299 if (bytesAlreadyRecieved != 0) {
300 reply = incomingMessage;
301 super.channelRead(ctx, reply);
304 // Alarm Streams never have a LastHttpContent as they always stay open//
305 else if (contentType.contains("multipart")) {
306 int beginIndex, endIndex;
307 if (bytesToRecieve == 0) {
308 beginIndex = incomingMessage.indexOf("Content-Length:");
309 if (beginIndex != -1) {
310 endIndex = incomingMessage.indexOf("\r\n", beginIndex);
311 if (endIndex != -1) {
312 bytesToRecieve = Integer.parseInt(
313 incomingMessage.substring(beginIndex + 15, endIndex).strip());
317 // --boundary and headers are not included in the Content-Length value
318 if (bytesAlreadyRecieved > bytesToRecieve) {
319 // Check if message has a second --boundary
320 endIndex = incomingMessage.indexOf("--" + boundary, bytesToRecieve);
321 if (endIndex == -1) {
322 reply = incomingMessage;
323 incomingMessage = "";
325 bytesAlreadyRecieved = 0;
327 reply = incomingMessage.substring(0, endIndex);
328 incomingMessage = incomingMessage.substring(endIndex, incomingMessage.length());
329 bytesToRecieve = 0;// Triggers search next time for Content-Length:
330 bytesAlreadyRecieved = incomingMessage.length() - endIndex;
332 super.channelRead(ctx, reply);
335 // Foscam needs this as will other cameras with chunks//
336 if (isChunked && bytesAlreadyRecieved != 0) {
337 logger.debug("Reply is chunked.");
338 reply = incomingMessage;
339 super.channelRead(ctx, reply);
343 } else { // msg is not HttpContent
344 // Foscam cameras need this
345 if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
346 reply = incomingMessage;
347 logger.debug("Packet back from camera is {}", incomingMessage);
348 super.channelRead(ctx, reply);
352 ReferenceCountUtil.release(msg);
357 public void channelReadComplete(@Nullable ChannelHandlerContext ctx) {
361 public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
365 public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
369 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
370 if (cause == null || ctx == null) {
373 if (cause instanceof ArrayIndexOutOfBoundsException) {
374 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
377 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
384 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
388 if (evt instanceof IdleStateEvent) {
389 IdleStateEvent e = (IdleStateEvent) evt;
390 // If camera does not use the channel for X amount of time it will close.
391 if (e.state() == IdleState.READER_IDLE) {
392 String urlToKeepOpen = "";
393 switch (thing.getThingTypeUID().getId()) {
395 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
397 case HIKVISION_THING:
398 urlToKeepOpen = "/ISAPI/Event/notification/alertStream";
401 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
404 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
405 if (channelTracking != null) {
406 if (channelTracking.getChannel() == ctx.channel()) {
407 return; // don't auto close this as it is for the alarms.
416 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
417 IpCameraDynamicStateDescriptionProvider stateDescriptionProvider, HttpService httpService) {
419 this.stateDescriptionProvider = stateDescriptionProvider;
420 if (ipAddress != null) {
423 hostIp = Helper.getLocalIpAddress();
425 this.groupTracker = groupTracker;
426 this.httpService = httpService;
429 private IpCameraHandler getHandle() {
433 // false clears the stored user/pass hash, true creates the hash
434 public boolean setBasicAuth(boolean useBasic) {
436 logger.debug("Clearing out the stored BASIC auth now.");
439 } else if (!basicAuth.isEmpty()) {
440 // If the binding sends multiple requests before basicAuth was set, this may trigger falsely.
441 logger.warn("Camera is reporting your username and/or password is wrong.");
444 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
445 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
446 ByteBuf byteBuf = null;
448 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
449 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
451 if (byteBuf != null) {
457 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
462 private String getCorrectUrlFormat(String longUrl) {
463 String temp = longUrl;
466 if (longUrl.isEmpty() || "ffmpeg".equals(longUrl)) {
471 url = new URL(longUrl);
472 int port = url.getPort();
474 if (url.getQuery() == null) {
475 temp = url.getPath();
477 temp = url.getPath() + "?" + url.getQuery();
480 if (url.getQuery() == null) {
481 temp = ":" + url.getPort() + url.getPath();
483 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
486 } catch (MalformedURLException e) {
487 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
492 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
493 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
494 sendHttpRequest("PUT", httpRequestURL, null);
497 public void sendHttpGET(String httpRequestURL) {
498 sendHttpRequest("GET", httpRequestURL, null);
501 public int getPortFromShortenedUrl(String httpRequestURL) {
502 if (httpRequestURL.startsWith(":")) {
503 int end = httpRequestURL.indexOf("/");
504 return Integer.parseInt(httpRequestURL.substring(1, end));
506 return cameraConfig.getPort();
509 public String getTinyUrl(String httpRequestURL) {
510 if (httpRequestURL.startsWith(":")) {
511 int beginIndex = httpRequestURL.indexOf("/");
512 return httpRequestURL.substring(beginIndex);
514 return httpRequestURL;
517 private void checkCameraConnection() {
518 Bootstrap localBootstrap = mainBootstrap;
519 if (localBootstrap != null) {
520 ChannelFuture chFuture = localBootstrap
521 .connect(new InetSocketAddress(cameraConfig.getIp(), cameraConfig.getPort()));
522 if (chFuture.awaitUninterruptibly(500)) {
523 chFuture.channel().close();
527 cameraCommunicationError(
528 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
531 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
532 // The authHandler will generate a digest string and re-send using this same function when needed.
533 @SuppressWarnings("null")
534 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
535 int port = getPortFromShortenedUrl(httpRequestURLFull);
536 String httpRequestURL = getTinyUrl(httpRequestURLFull);
538 if (mainBootstrap == null) {
539 mainBootstrap = new Bootstrap();
540 mainBootstrap.group(mainEventLoopGroup);
541 mainBootstrap.channel(NioSocketChannel.class);
542 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
543 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
544 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
545 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
546 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
547 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
550 public void initChannel(SocketChannel socketChannel) throws Exception {
551 // HIK Alarm stream needs > 9sec idle to stop stream closing
552 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
553 socketChannel.pipeline().addLast(new HttpClientCodec());
554 socketChannel.pipeline().addLast(AUTH_HANDLER,
555 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
556 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
558 switch (thing.getThingTypeUID().getId()) {
560 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
563 socketChannel.pipeline()
564 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
567 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
570 socketChannel.pipeline().addLast(
571 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
573 case HIKVISION_THING:
574 socketChannel.pipeline()
575 .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
578 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
581 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
588 FullHttpRequest request;
589 if (!"PUT".equals(httpMethod) || (useDigestAuth && digestString == null)) {
590 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
591 request.headers().set("Host", cameraConfig.getIp() + ":" + port);
592 request.headers().set("Connection", HttpHeaderValues.KEEP_ALIVE);
594 request = putRequestWithBody;
597 if (!basicAuth.isEmpty()) {
599 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
602 request.headers().set("Authorization", "Basic " + basicAuth);
607 if (digestString != null) {
608 request.headers().set("Authorization", "Digest " + digestString);
612 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
613 .addListener(new ChannelFutureListener() {
616 public void operationComplete(@Nullable ChannelFuture future) {
617 if (future == null) {
620 if (future.isDone() && future.isSuccess()) {
621 Channel ch = future.channel();
622 openChannels.add(ch);
626 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
629 openChannel(ch, httpRequestURL);
630 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
631 commonHandler.setURL(httpRequestURLFull);
632 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
633 authHandler.setURL(httpMethod, httpRequestURL);
635 switch (thing.getThingTypeUID().getId()) {
637 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
638 amcrestHandler.setURL(httpRequestURL);
641 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
642 instarHandler.setURL(httpRequestURL);
645 ch.writeAndFlush(request);
646 } else { // an error occured
647 cameraCommunicationError(
648 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
654 public void processSnapshot(byte[] incommingSnapshot) {
655 lockCurrentSnapshot.lock();
657 currentSnapshot = incommingSnapshot;
658 if (cameraConfig.getGifPreroll() > 0) {
659 fifoSnapshotBuffer.add(incommingSnapshot);
660 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
661 fifoSnapshotBuffer.removeFirst();
665 lockCurrentSnapshot.unlock();
666 currentSnapshotTime = Instant.now();
669 if (updateImageChannel) {
670 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
671 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
672 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
673 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
674 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
675 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
676 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
680 public void startStreamServer() {
681 servlet = new CameraServlet(this, httpService);
682 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
683 + getThing().getUID().getId() + "/ipcamera.m3u8"));
684 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
685 + getThing().getUID().getId() + "/ipcamera.jpg"));
686 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
687 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
690 public void openCamerasStream() {
691 threadPool.schedule(this::openMjpegStream, 500, TimeUnit.MILLISECONDS);
694 private void openMjpegStream() {
695 sendHttpGET(mjpegUri);
698 private void openChannel(Channel channel, String httpRequestURL) {
699 ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
700 if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
701 tracker.setChannel(channel);
704 channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
707 public void closeChannel(String url) {
708 ChannelTracking channelTracking = channelTrackingMap.get(url);
709 if (channelTracking != null) {
710 if (channelTracking.getChannel().isOpen()) {
711 channelTracking.getChannel().close();
718 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
719 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
722 void cleanChannels() {
723 for (Channel channel : openChannels) {
724 boolean oldChannel = true;
725 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
726 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
727 channelTrackingMap.remove(channelTracking.getRequestUrl());
729 if (channelTracking.getChannel() == channel) {
730 logger.trace("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
740 public void storeHttpReply(String url, String content) {
741 ChannelTracking channelTracking = channelTrackingMap.get(url);
742 if (channelTracking != null) {
743 channelTracking.setReply(content);
747 private void storeSnapshots() {
749 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
750 lockCurrentSnapshot.lock();
752 for (byte[] foo : fifoSnapshotBuffer) {
753 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
756 OutputStream fos = new FileOutputStream(file);
759 } catch (FileNotFoundException e) {
760 logger.warn("FileNotFoundException {}", e.getMessage());
761 } catch (IOException e) {
762 logger.warn("IOException {}", e.getMessage());
766 lockCurrentSnapshot.unlock();
770 public void setupFfmpegFormat(FFmpegFormat format) {
771 String inputOptions = cameraConfig.getFfmpegInputOptions();
772 if (cameraConfig.getFfmpegOutput().isEmpty()) {
773 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
776 if (rtspUri.isEmpty()) {
777 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
780 if (cameraConfig.getFfmpegLocation().isEmpty()) {
781 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
784 if (rtspUri.toLowerCase().contains("rtsp")) {
785 if (inputOptions.isEmpty()) {
786 inputOptions = "-rtsp_transport tcp";
790 // Make sure the folder exists, if not create it.
791 new File(cameraConfig.getFfmpegOutput()).mkdirs();
794 if (ffmpegHLS == null) {
795 if (!inputOptions.isEmpty()) {
796 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
797 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
798 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
799 cameraConfig.getUser(), cameraConfig.getPassword());
801 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
802 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
803 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
804 cameraConfig.getPassword());
807 Ffmpeg localHLS = ffmpegHLS;
808 if (localHLS != null) {
809 localHLS.startConverting();
813 if (cameraConfig.getGifPreroll() > 0) {
814 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
815 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
816 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
817 + cameraConfig.getGifOutOptions(),
818 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
819 cameraConfig.getPassword());
821 if (!inputOptions.isEmpty()) {
822 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
824 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
826 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
827 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
828 cameraConfig.getUser(), cameraConfig.getPassword());
830 if (cameraConfig.getGifPreroll() > 0) {
833 Ffmpeg localGIF = ffmpegGIF;
834 if (localGIF != null) {
835 localGIF.startConverting();
836 if (gifHistory.isEmpty()) {
837 gifHistory = gifFilename;
838 } else if (!"ipcamera".equals(gifFilename)) {
839 gifHistory = gifFilename + "," + gifHistory;
840 if (gifHistoryLength > 49) {
841 int endIndex = gifHistory.lastIndexOf(",");
842 gifHistory = gifHistory.substring(0, endIndex);
845 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
849 if (!inputOptions.isEmpty()) {
850 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
852 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
854 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
855 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
856 cameraConfig.getUser(), cameraConfig.getPassword());
857 Ffmpeg localRecord = ffmpegRecord;
858 if (localRecord != null) {
859 localRecord.startConverting();
860 if (mp4History.isEmpty()) {
861 mp4History = mp4Filename;
862 } else if (!"ipcamera".equals(mp4Filename)) {
863 mp4History = mp4Filename + "," + mp4History;
864 if (mp4HistoryLength > 49) {
865 int endIndex = mp4History.lastIndexOf(",");
866 mp4History = mp4History.substring(0, endIndex);
870 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
873 Ffmpeg localAlarms = ffmpegRtspHelper;
874 if (localAlarms != null) {
875 localAlarms.stopConverting();
876 if (!audioAlarmEnabled && !motionAlarmEnabled) {
880 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
881 String filterOptions = "";
882 if (!audioAlarmEnabled) {
883 filterOptions = "-an";
885 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
887 if (!motionAlarmEnabled && !ffmpegSnapshotGeneration) {
888 filterOptions = filterOptions.concat(" -vn");
889 } else if (motionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
890 String usersMotionOptions = cameraConfig.getMotionOptions();
891 if (usersMotionOptions.startsWith("-")) {
892 // Need to put the users custom options first in the chain before the motion is detected
893 filterOptions += " " + usersMotionOptions + ",select='gte(scene,"
894 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
896 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
897 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
899 } else if (motionAlarmEnabled) {
900 filterOptions = filterOptions.concat(" -vf select='gte(scene,"
901 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print");
903 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
904 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
905 localAlarms = ffmpegRtspHelper;
906 if (localAlarms != null) {
907 localAlarms.startConverting();
911 if (ffmpegMjpeg == null) {
912 if (inputOptions.isEmpty()) {
913 inputOptions = "-hide_banner -loglevel warning";
915 inputOptions += " -hide_banner -loglevel warning";
917 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
918 cameraConfig.getMjpegOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
919 + getThing().getUID().getId() + "/ipcamera.jpg",
920 cameraConfig.getUser(), cameraConfig.getPassword());
922 Ffmpeg localMjpeg = ffmpegMjpeg;
923 if (localMjpeg != null) {
924 localMjpeg.startConverting();
928 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
929 if (ffmpegSnapshot == null) {
930 if (inputOptions.isEmpty()) {
932 inputOptions = "-threads 1 -skip_frame nokey -hide_banner -loglevel warning";
934 inputOptions += " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
936 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
937 cameraConfig.getSnapshotOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
938 + getThing().getUID().getId() + "/snapshot.jpg",
939 cameraConfig.getUser(), cameraConfig.getPassword());
941 Ffmpeg localSnaps = ffmpegSnapshot;
942 if (localSnaps != null) {
943 localSnaps.startConverting();
949 public void noMotionDetected(String thisAlarmsChannel) {
950 setChannelState(thisAlarmsChannel, OnOffType.OFF);
951 firstMotionAlarm = false;
952 motionAlarmUpdateSnapshot = false;
953 motionDetected = false;
954 if (streamingAutoFps) {
955 stopSnapshotPolling();
956 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
957 stopSnapshotPolling();
962 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
963 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
964 * tampering with the camera.
966 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
967 updateState(thisAlarmsChannel, state);
970 public void motionDetected(String thisAlarmsChannel) {
971 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
972 updateState(thisAlarmsChannel, OnOffType.ON);
973 motionDetected = true;
974 if (streamingAutoFps) {
975 startSnapshotPolling();
977 if (cameraConfig.getUpdateImageWhen().contains("2")) {
978 if (!firstMotionAlarm) {
979 if (!snapshotUri.isEmpty()) {
980 sendHttpGET(snapshotUri);
982 firstMotionAlarm = true;// reset back to false when the jpg arrives.
984 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
985 if (!snapshotPolling) {
986 startSnapshotPolling();
988 firstMotionAlarm = true;
989 motionAlarmUpdateSnapshot = true;
993 public void audioDetected() {
994 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
995 if (cameraConfig.getUpdateImageWhen().contains("3")) {
996 if (!firstAudioAlarm) {
997 if (!snapshotUri.isEmpty()) {
998 sendHttpGET(snapshotUri);
1000 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1002 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1003 firstAudioAlarm = true;
1004 audioAlarmUpdateSnapshot = true;
1008 public void noAudioDetected() {
1009 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1010 firstAudioAlarm = false;
1011 audioAlarmUpdateSnapshot = false;
1014 public void recordMp4(String filename, int seconds) {
1015 mp4Filename = filename;
1016 mp4RecordTime = seconds;
1017 setupFfmpegFormat(FFmpegFormat.RECORD);
1018 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1021 public void recordGif(String filename, int seconds) {
1022 gifFilename = filename;
1023 gifRecordTime = seconds;
1024 if (cameraConfig.getGifPreroll() > 0) {
1025 snapCount = seconds;
1027 setupFfmpegFormat(FFmpegFormat.GIF);
1029 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1032 public String returnValueFromString(String rawString, String searchedString) {
1034 int index = rawString.indexOf(searchedString);
1035 if (index != -1) // -1 means "not found"
1037 result = rawString.substring(index + searchedString.length(), rawString.length());
1038 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1040 return result; // Did not find a carriage return.
1042 return result.substring(0, index);
1045 return ""; // Did not find the String we were searching for
1048 private void sendPTZRequest() {
1049 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1053 public void channelLinked(ChannelUID channelUID) {
1054 switch (channelUID.getId()) {
1055 case CHANNEL_MJPEG_URL:
1056 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1057 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
1059 case CHANNEL_HLS_URL:
1060 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1061 + getThing().getUID().getId() + "/ipcamera.m3u8"));
1063 case CHANNEL_IMAGE_URL:
1064 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1065 + getThing().getUID().getId() + "/ipcamera.jpg"));
1071 public void handleCommand(ChannelUID channelUID, Command command) {
1072 if (command instanceof RefreshType) {
1073 switch (channelUID.getId()) {
1075 if (onvifCamera.supportsPTZ()) {
1076 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1080 if (onvifCamera.supportsPTZ()) {
1081 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1085 if (onvifCamera.supportsPTZ()) {
1086 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1089 case CHANNEL_GOTO_PRESET:
1090 if (onvifCamera.supportsPTZ()) {
1091 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1095 } // caution "REFRESH" can still progress to brand Handlers below the else.
1097 switch (channelUID.getId()) {
1098 case CHANNEL_MP4_HISTORY_LENGTH:
1099 if (DecimalType.ZERO.equals(command)) {
1100 mp4HistoryLength = 0;
1102 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1105 case CHANNEL_GIF_HISTORY_LENGTH:
1106 if (DecimalType.ZERO.equals(command)) {
1107 gifHistoryLength = 0;
1109 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1112 case CHANNEL_FFMPEG_MOTION_CONTROL:
1113 if (OnOffType.ON.equals(command)) {
1114 motionAlarmEnabled = true;
1115 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1116 motionAlarmEnabled = false;
1117 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1118 } else if (command instanceof PercentType) {
1119 motionAlarmEnabled = true;
1120 motionThreshold = ((PercentType) command).toBigDecimal();
1122 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1124 case CHANNEL_START_STREAM:
1126 if (OnOffType.ON.equals(command)) {
1127 localHLS = ffmpegHLS;
1128 if (localHLS == null) {
1129 setupFfmpegFormat(FFmpegFormat.HLS);
1130 localHLS = ffmpegHLS;
1132 if (localHLS != null) {
1133 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1134 localHLS.startConverting();
1137 localHLS = ffmpegHLS;
1138 if (localHLS != null) {
1139 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1140 localHLS.setKeepAlive(1);
1144 case CHANNEL_EXTERNAL_MOTION:
1145 if (OnOffType.ON.equals(command)) {
1146 motionDetected(CHANNEL_EXTERNAL_MOTION);
1148 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1151 case CHANNEL_GOTO_PRESET:
1152 if (onvifCamera.supportsPTZ()) {
1153 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1156 case CHANNEL_POLL_IMAGE:
1157 if (OnOffType.ON.equals(command)) {
1158 if (snapshotUri.isEmpty()) {
1159 ffmpegSnapshotGeneration = true;
1160 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1161 updateImageChannel = false;
1163 updateImageChannel = true;
1164 sendHttpGET(snapshotUri);// Allows this to change Image FPS on demand
1167 Ffmpeg localSnaps = ffmpegSnapshot;
1168 if (localSnaps != null) {
1169 localSnaps.stopConverting();
1170 ffmpegSnapshotGeneration = false;
1172 updateImageChannel = false;
1176 if (onvifCamera.supportsPTZ()) {
1177 if (command instanceof IncreaseDecreaseType) {
1178 if (command == IncreaseDecreaseType.INCREASE) {
1179 if (cameraConfig.getPtzContinuous()) {
1180 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1182 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1185 if (cameraConfig.getPtzContinuous()) {
1186 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1188 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1192 } else if (OnOffType.OFF.equals(command)) {
1193 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1196 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1197 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1201 if (onvifCamera.supportsPTZ()) {
1202 if (command instanceof IncreaseDecreaseType) {
1203 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1204 if (cameraConfig.getPtzContinuous()) {
1205 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1207 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1210 if (cameraConfig.getPtzContinuous()) {
1211 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1213 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1217 } else if (OnOffType.OFF.equals(command)) {
1218 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1221 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1222 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1226 if (onvifCamera.supportsPTZ()) {
1227 if (command instanceof IncreaseDecreaseType) {
1228 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1229 if (cameraConfig.getPtzContinuous()) {
1230 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1232 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1235 if (cameraConfig.getPtzContinuous()) {
1236 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1238 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1242 } else if (OnOffType.OFF.equals(command)) {
1243 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1246 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1247 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1252 // commands and refresh now get passed to brand handlers
1253 switch (thing.getThingTypeUID().getId()) {
1255 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1256 amcrestHandler.handleCommand(channelUID, command);
1257 if (lowPriorityRequests.isEmpty()) {
1258 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1262 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1263 dahuaHandler.handleCommand(channelUID, command);
1264 if (lowPriorityRequests.isEmpty()) {
1265 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1268 case DOORBIRD_THING:
1269 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1270 doorBirdHandler.handleCommand(channelUID, command);
1271 if (lowPriorityRequests.isEmpty()) {
1272 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1275 case HIKVISION_THING:
1276 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1277 hikvisionHandler.handleCommand(channelUID, command);
1278 if (lowPriorityRequests.isEmpty()) {
1279 lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1283 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1284 cameraConfig.getPassword());
1285 foscamHandler.handleCommand(channelUID, command);
1286 if (lowPriorityRequests.isEmpty()) {
1287 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1291 InstarHandler instarHandler = new InstarHandler(getHandle());
1292 instarHandler.handleCommand(channelUID, command);
1293 if (lowPriorityRequests.isEmpty()) {
1294 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1298 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1299 defaultHandler.handleCommand(channelUID, command);
1300 if (lowPriorityRequests.isEmpty()) {
1301 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1307 public void setChannelState(String channelToUpdate, State valueOf) {
1308 updateState(channelToUpdate, valueOf);
1311 private void bringCameraOnline() {
1313 updateStatus(ThingStatus.ONLINE);
1314 groupTracker.listOfOnlineCameraHandlers.add(this);
1315 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1316 Future<?> localFuture = cameraConnectionJob;
1317 if (localFuture != null) {
1318 localFuture.cancel(false);
1320 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1321 snapshotPolling = true;
1322 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000, cameraConfig.getPollTime(),
1323 TimeUnit.MILLISECONDS);
1326 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1328 if (!rtspUri.isEmpty()) {
1329 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1331 if (updateImageChannel) {
1332 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1334 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1336 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1337 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1338 handle.cameraOnline(getThing().getUID().getId());
1343 void snapshotIsFfmpeg() {
1344 bringCameraOnline();
1345 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1347 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1348 if (!rtspUri.isEmpty()) {
1349 updateImageChannel = false;
1350 ffmpegSnapshotGeneration = true;
1351 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1352 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1354 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1358 void pollingCameraConnection() {
1359 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1360 if (rtspUri.isEmpty()) {
1361 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1363 if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1366 sendHttpRequest("GET", snapshotUri, null);
1370 if (!onvifCamera.isConnected()) {
1371 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1372 cameraConfig.getOnvifPort());
1373 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1375 if ("ffmpeg".equals(snapshotUri)) {
1377 } else if (!snapshotUri.isEmpty()) {
1378 sendHttpRequest("GET", snapshotUri, null);
1379 } else if (!rtspUri.isEmpty()) {
1382 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1383 "Camera failed to report a valid Snaphot and/or RTSP URL. See readme on how to use the SNAPSHOT_URL_OVERRIDE feature.");
1387 public void cameraConfigError(String reason) {
1388 // wont try to reconnect again due to a config error being the cause.
1389 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1393 public void cameraCommunicationError(String reason) {
1394 // will try to reconnect again as camera may be rebooting.
1395 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1396 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1397 resetAndRetryConnecting();
1401 boolean streamIsStopped(String url) {
1402 ChannelTracking channelTracking = channelTrackingMap.get(url);
1403 if (channelTracking != null) {
1404 if (channelTracking.getChannel().isActive()) {
1405 return false; // stream is running.
1408 return true; // Stream stopped or never started.
1411 void snapshotRunnable() {
1412 // Snapshot should be first to keep consistent time between shots
1413 sendHttpGET(snapshotUri);
1414 if (snapCount > 0) {
1415 if (--snapCount == 0) {
1416 setupFfmpegFormat(FFmpegFormat.GIF);
1421 public byte[] getSnapshot() {
1423 // Keep streams open when the camera goes offline so they dont stop.
1424 return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
1425 0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43,
1426 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
1427 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09,
1428 0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d,
1429 0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10,
1430 0x10, (byte) 0xff, (byte) 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
1431 (byte) 0xff, (byte) 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, (byte) 0xff, (byte) 0xda, 0x00, 0x08,
1432 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, (byte) 0xd2, (byte) 0xcf, 0x20, (byte) 0xff, (byte) 0xd9 };
1434 if (!snapshotPolling && !ffmpegSnapshotGeneration) {
1435 sendHttpGET(snapshotUri);
1437 lockCurrentSnapshot.lock();
1439 return currentSnapshot;
1441 lockCurrentSnapshot.unlock();
1445 public void stopSnapshotPolling() {
1446 Future<?> localFuture;
1447 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1448 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1449 snapshotPolling = false;
1450 localFuture = snapshotJob;
1451 if (localFuture != null) {
1452 localFuture.cancel(true);
1454 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1455 snapshotPolling = false;
1456 localFuture = snapshotJob;
1457 if (localFuture != null) {
1458 localFuture.cancel(true);
1463 public void startSnapshotPolling() {
1464 if (snapshotPolling || ffmpegSnapshotGeneration) {
1465 return; // Already polling or creating with FFmpeg from RTSP
1467 if (streamingSnapshotMjpeg || streamingAutoFps) {
1468 snapshotPolling = true;
1469 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1470 TimeUnit.MILLISECONDS);
1471 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1472 snapshotPolling = true;
1473 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1474 TimeUnit.MILLISECONDS);
1479 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep mjpeg and alarm
1480 * streams open and more.
1483 void pollCameraRunnable() {
1484 if (!snapshotUri.isEmpty() && !snapshotPolling) {// we need to check camera is still online.
1485 checkCameraConnection();
1487 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1488 if (!lowPriorityRequests.isEmpty()) {
1489 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1490 lowPriorityCounter = 0;
1492 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1494 // what needs to be done every poll//
1495 switch (thing.getThingTypeUID().getId()) {
1499 if (!onvifCamera.isConnected()) {
1500 onvifCamera.connect(true);
1504 noMotionDetected(CHANNEL_MOTION_ALARM);
1505 noMotionDetected(CHANNEL_PIR_ALARM);
1508 case HIKVISION_THING:
1509 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1510 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1511 cameraConfig.getIp());
1512 sendHttpGET("/ISAPI/Event/notification/alertStream");
1516 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1517 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1520 // Check for alarms, channel for NVRs appears not to work at filtering.
1521 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1522 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1523 cameraConfig.getIp());
1524 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1527 case DOORBIRD_THING:
1528 // Check for alarms, channel for NVRs appears not to work at filtering.
1529 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1530 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1531 cameraConfig.getIp());
1532 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1536 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1537 + cameraConfig.getPassword());
1540 Ffmpeg localHLS = ffmpegHLS;
1541 if (localHLS != null) {
1542 localHLS.checkKeepAlive();
1544 if (openChannels.size() > 18) {
1545 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1551 public void initialize() {
1552 cameraConfig = getConfigAs(CameraConfig.class);
1553 threadPool = Executors.newScheduledThreadPool(4);
1554 mainEventLoopGroup = new NioEventLoopGroup(3);
1555 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1556 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1557 rtspUri = cameraConfig.getFfmpegInput();
1558 if (cameraConfig.getFfmpegOutput().isEmpty()) {
1560 .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1562 // Known cameras will connect quicker if we skip ONVIF questions.
1563 switch (thing.getThingTypeUID().getId()) {
1566 if (mjpegUri.isEmpty()) {
1567 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1569 if (snapshotUri.isEmpty()) {
1570 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1573 case DOORBIRD_THING:
1574 if (mjpegUri.isEmpty()) {
1575 mjpegUri = "/bha-api/video.cgi";
1577 if (snapshotUri.isEmpty()) {
1578 snapshotUri = "/bha-api/image.cgi";
1582 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1583 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1584 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1585 if (mjpegUri.isEmpty()) {
1586 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1587 + cameraConfig.getPassword();
1589 if (snapshotUri.isEmpty()) {
1590 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1591 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1594 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1595 if (mjpegUri.isEmpty()) {
1596 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1598 if (snapshotUri.isEmpty()) {
1599 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1603 if (snapshotUri.isEmpty()) {
1604 snapshotUri = "/tmpfs/snap.jpg";
1606 if (mjpegUri.isEmpty()) {
1607 mjpegUri = "/mjpegstream.cgi?-chn=12";
1610 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
1611 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1612 + getThing().getUID().getId()
1613 + "/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");
1616 // for poll times 9 seconds and above don't display a warning about the Image channel.
1617 if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1619 "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.");
1621 // ONVIF and Instar event handling need the server started before connecting.
1622 startStreamServer();
1626 private void tryConnecting() {
1627 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1628 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1629 cameraConfig.getUser(), cameraConfig.getPassword());
1630 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1631 // Only use ONVIF events if it is not an API camera.
1632 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1634 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 30, TimeUnit.SECONDS);
1637 // What the camera needs to re-connect if the initialize() is not called.
1638 private void resetAndRetryConnecting() {
1643 private void offline() {
1645 snapshotPolling = false;
1646 Future<?> localFuture = pollCameraJob;
1647 if (localFuture != null) {
1648 localFuture.cancel(true);
1651 localFuture = snapshotJob;
1652 if (localFuture != null) {
1653 localFuture.cancel(true);
1656 localFuture = cameraConnectionJob;
1657 if (localFuture != null) {
1658 localFuture.cancel(true);
1661 Ffmpeg localFfmpeg = ffmpegHLS;
1662 if (localFfmpeg != null) {
1663 localFfmpeg.stopConverting();
1666 localFfmpeg = ffmpegRecord;
1667 if (localFfmpeg != null) {
1668 localFfmpeg.stopConverting();
1669 ffmpegRecord = null;
1671 localFfmpeg = ffmpegGIF;
1672 if (localFfmpeg != null) {
1673 localFfmpeg.stopConverting();
1676 localFfmpeg = ffmpegRtspHelper;
1677 if (localFfmpeg != null) {
1678 localFfmpeg.stopConverting();
1679 ffmpegRtspHelper = null;
1681 localFfmpeg = ffmpegMjpeg;
1682 if (localFfmpeg != null) {
1683 localFfmpeg.stopConverting();
1686 localFfmpeg = ffmpegSnapshot;
1687 if (localFfmpeg != null) {
1688 localFfmpeg.stopConverting();
1689 ffmpegSnapshot = null;
1691 onvifCamera.disconnect();
1692 openChannels.close();
1696 public void dispose() {
1698 CameraServlet localServlet = servlet;
1699 if (localServlet != null) {
1700 localServlet.dispose();
1701 localServlet = null;
1703 threadPool.shutdown();
1704 // inform all group handlers that this camera has gone offline
1705 groupTracker.listOfOnlineCameraHandlers.remove(this);
1706 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1707 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1708 handle.cameraOffline(this);
1710 basicAuth = ""; // clear out stored Password hash
1711 useDigestAuth = false;
1712 mainEventLoopGroup.shutdownGracefully();
1713 mainBootstrap = null;
1714 channelTrackingMap.clear();
1717 public String getWhiteList() {
1718 return cameraConfig.getIpWhitelist();
1722 public Collection<Class<? extends ThingHandlerService>> getServices() {
1723 return Collections.singleton(IpCameraActions.class);