2 * Copyright (c) 2010-2023 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 boolean newInstarApi = false;
174 public String snapshotUri = "";
175 public String mjpegUri = "";
176 private byte[] currentSnapshot = new byte[] { (byte) 0x00 };
177 public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
178 public String rtspUri = "";
179 public boolean audioAlarmUpdateSnapshot = false;
180 private boolean motionAlarmUpdateSnapshot = false;
181 private boolean isOnline = false; // Used so only 1 error is logged when a network issue occurs.
182 private boolean firstAudioAlarm = false;
183 private boolean firstMotionAlarm = false;
184 public BigDecimal motionThreshold = BigDecimal.ZERO;
185 public int audioThreshold = 35;
186 public boolean streamingSnapshotMjpeg = false;
187 public boolean ffmpegMotionAlarmEnabled = false;
188 public boolean ffmpegAudioAlarmEnabled = false;
189 public boolean ffmpegSnapshotGeneration = false;
190 public boolean snapshotPolling = false;
191 public OnvifConnection onvifCamera = new OnvifConnection(this, "", "", "");
193 // These methods handle the response from all camera brands, nothing specific to 1 brand.
194 private class CommonCameraHandler extends ChannelDuplexHandler {
195 private int bytesToRecieve = 0;
196 private int bytesAlreadyRecieved = 0;
197 private byte[] incomingJpeg = new byte[0];
198 private String incomingMessage = "";
199 private String contentType = "empty";
200 private String boundary = "";
201 private Object reply = new Object();
202 private String requestUrl = "";
203 private boolean isChunked = false;
205 public void setURL(String url) {
210 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
211 if (msg == null || ctx == null) {
215 if (msg instanceof HttpResponse) {
216 HttpResponse response = (HttpResponse) msg;
217 if (response.status().code() == 200) {
218 if (!response.headers().isEmpty()) {
219 for (String name : response.headers().names()) {
220 // Some cameras use first letter uppercase and others dont.
221 switch (name.toLowerCase()) { // Possible localization issues doing this
223 contentType = response.headers().getAsString(name);
225 case "content-length":
226 bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
228 case "transfer-encoding":
229 if (response.headers().getAsString(name).contains("chunked")) {
235 if (contentType.contains("multipart")) {
236 boundary = Helper.searchString(contentType, "boundary=");
237 if (mjpegUri.equals(requestUrl)) {
238 if (msg instanceof HttpMessage) {
239 // very start of stream only
240 mjpegContentType = contentType;
241 CameraServlet localServlet = servlet;
242 if (localServlet != null) {
243 logger.debug("Setting Content-Type to:{}", contentType);
244 localServlet.openStreams.updateContentType(contentType, boundary);
248 } else if (contentType.contains("image/jp")) {
249 if (bytesToRecieve == 0) {
250 bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
251 logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
253 incomingJpeg = new byte[bytesToRecieve];
257 // Non 200 OK replies are logged and handled in pipeline by MyNettyAuthHandler.java
261 if (msg instanceof HttpContent) {
262 HttpContent content = (HttpContent) msg;
263 if (mjpegUri.equals(requestUrl) && !(content instanceof LastHttpContent)) {
264 // multiple MJPEG stream packets come back as this.
265 byte[] chunkedFrame = new byte[content.content().readableBytes()];
266 content.content().getBytes(content.content().readerIndex(), chunkedFrame);
267 CameraServlet localServlet = servlet;
268 if (localServlet != null) {
269 localServlet.openStreams.queueFrame(chunkedFrame);
272 // Found some cameras use Content-Type: image/jpg instead of image/jpeg
273 if (contentType.contains("image/jp")) {
274 for (int i = 0; i < content.content().capacity(); i++) {
275 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
277 if (content instanceof LastHttpContent) {
278 processSnapshot(incomingJpeg);
281 } else { // incomingMessage that is not an IMAGE
282 if (incomingMessage.isEmpty()) {
283 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
285 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
287 bytesAlreadyRecieved = incomingMessage.length();
288 if (content instanceof LastHttpContent) {
289 // If it is not an image send it on to the next handler//
290 if (bytesAlreadyRecieved != 0) {
291 reply = incomingMessage;
292 super.channelRead(ctx, reply);
295 // Alarm Streams never have a LastHttpContent as they always stay open//
296 else if (contentType.contains("multipart")) {
297 int beginIndex, endIndex;
298 if (bytesToRecieve == 0) {
299 beginIndex = incomingMessage.indexOf("Content-Length:");
300 if (beginIndex != -1) {
301 endIndex = incomingMessage.indexOf("\r\n", beginIndex);
302 if (endIndex != -1) {
303 bytesToRecieve = Integer.parseInt(
304 incomingMessage.substring(beginIndex + 15, endIndex).strip());
308 // --boundary and headers are not included in the Content-Length value
309 if (bytesAlreadyRecieved > bytesToRecieve) {
310 // Check if message has a second --boundary
311 endIndex = incomingMessage.indexOf("--" + boundary, bytesToRecieve);
312 if (endIndex == -1) {
313 reply = incomingMessage;
314 incomingMessage = "";
316 bytesAlreadyRecieved = 0;
318 reply = incomingMessage.substring(0, endIndex);
319 incomingMessage = incomingMessage.substring(endIndex, incomingMessage.length());
320 bytesToRecieve = 0;// Triggers search next time for Content-Length:
321 bytesAlreadyRecieved = incomingMessage.length() - endIndex;
323 super.channelRead(ctx, reply);
326 // Foscam needs this as will other cameras with chunks//
327 if (isChunked && bytesAlreadyRecieved != 0) {
328 logger.debug("Reply is chunked.");
329 reply = incomingMessage;
330 super.channelRead(ctx, reply);
334 } else { // msg is not HttpContent
335 // Foscam cameras need this
336 if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
337 reply = incomingMessage;
338 logger.debug("Packet back from camera is {}", incomingMessage);
339 super.channelRead(ctx, reply);
343 ReferenceCountUtil.release(msg);
348 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
349 if (cause == null || ctx == null) {
352 if (cause instanceof ArrayIndexOutOfBoundsException) {
353 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
356 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
363 @SuppressWarnings("PMD.CompareObjectsWithEquals")
364 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
368 if (evt instanceof IdleStateEvent) {
369 IdleStateEvent e = (IdleStateEvent) evt;
370 // If camera does not use the channel for X amount of time it will close.
371 if (e.state() == IdleState.READER_IDLE) {
372 String urlToKeepOpen = "";
373 switch (thing.getThingTypeUID().getId()) {
375 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
378 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
381 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
382 if (channelTracking != null) {
383 if (channelTracking.getChannel() == ctx.channel()) {
384 return; // don't auto close this as it is for the alarms.
387 logger.debug("Closing an idle channel for camera:{}", cameraConfig.getIp());
394 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
395 IpCameraDynamicStateDescriptionProvider stateDescriptionProvider, HttpService httpService) {
397 this.stateDescriptionProvider = stateDescriptionProvider;
398 if (ipAddress != null) {
401 hostIp = Helper.getLocalIpAddress();
403 this.groupTracker = groupTracker;
404 this.httpService = httpService;
407 private IpCameraHandler getHandle() {
411 // false clears the stored user/pass hash, true creates the hash
412 public boolean setBasicAuth(boolean useBasic) {
414 logger.debug("Clearing out the stored BASIC auth now.");
417 } else if (!basicAuth.isEmpty()) {
418 // If the binding sends multiple requests before basicAuth was set, this may trigger falsely.
419 logger.warn("Camera is reporting your username and/or password is wrong.");
422 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
423 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
424 ByteBuf byteBuf = null;
426 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
427 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
429 if (byteBuf != null) {
435 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
440 private String getCorrectUrlFormat(String longUrl) {
441 String temp = longUrl;
444 if (longUrl.isEmpty() || "ffmpeg".equals(longUrl)) {
449 url = new URL(longUrl);
450 int port = url.getPort();
452 if (url.getQuery() == null) {
453 temp = url.getPath();
455 temp = url.getPath() + "?" + url.getQuery();
458 if (url.getQuery() == null) {
459 temp = ":" + url.getPort() + url.getPath();
461 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
464 } catch (MalformedURLException e) {
465 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
470 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
471 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
472 sendHttpRequest("PUT", httpRequestURL, null);
475 public void sendHttpGET(String httpRequestURL) {
476 sendHttpRequest("GET", httpRequestURL, null);
479 public int getPortFromShortenedUrl(String httpRequestURL) {
480 if (httpRequestURL.startsWith(":")) {
481 int end = httpRequestURL.indexOf("/");
482 return Integer.parseInt(httpRequestURL.substring(1, end));
484 return cameraConfig.getPort();
487 public String getTinyUrl(String httpRequestURL) {
488 if (httpRequestURL.startsWith(":")) {
489 int beginIndex = httpRequestURL.indexOf("/");
490 return httpRequestURL.substring(beginIndex);
492 return httpRequestURL;
495 private void checkCameraConnection() {
496 if (snapshotPolling) {// Currently polling a real URL for snapshots, so camera must be online.
498 } else if (ffmpegSnapshotGeneration) {// Use RTSP stream creating snapshots to know camera is online.
499 Ffmpeg localSnapshot = ffmpegSnapshot;
500 if (localSnapshot != null && !localSnapshot.getIsAlive()) {
501 cameraCommunicationError("FFmpeg Snapshots Stopped: Check your camera can be reached.");
504 return;// ffmpeg snapshot stream is still alive
506 // Open a HTTP connection without sending any requests as we do not need a snapshot.
507 Bootstrap localBootstrap = mainBootstrap;
508 if (localBootstrap != null) {
509 ChannelFuture chFuture = localBootstrap
510 .connect(new InetSocketAddress(cameraConfig.getIp(), cameraConfig.getPort()));
511 if (chFuture.awaitUninterruptibly(500)) {
512 chFuture.channel().close();
516 cameraCommunicationError(
517 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
520 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
521 // The authHandler will generate a digest string and re-send using this same function when needed.
522 @SuppressWarnings("null")
523 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
524 int port = getPortFromShortenedUrl(httpRequestURLFull);
525 String httpRequestURL = getTinyUrl(httpRequestURLFull);
527 if (mainBootstrap == null) {
528 mainBootstrap = new Bootstrap();
529 mainBootstrap.group(mainEventLoopGroup);
530 mainBootstrap.channel(NioSocketChannel.class);
531 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
532 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
533 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
534 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
535 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
536 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
539 public void initChannel(SocketChannel socketChannel) throws Exception {
540 // HIK Alarm stream needs > 9sec idle to stop stream closing
541 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
542 socketChannel.pipeline().addLast(new HttpClientCodec());
543 socketChannel.pipeline().addLast(AUTH_HANDLER,
544 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
545 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
547 switch (thing.getThingTypeUID().getId()) {
549 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
552 socketChannel.pipeline()
553 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
556 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
559 socketChannel.pipeline().addLast(
560 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
562 case HIKVISION_THING:
563 socketChannel.pipeline()
564 .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
567 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
570 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
577 FullHttpRequest request;
578 if (!"PUT".equals(httpMethod) || (useDigestAuth && digestString == null)) {
579 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
580 request.headers().set("Host", cameraConfig.getIp() + ":" + port);
581 request.headers().set("Connection", HttpHeaderValues.CLOSE);
583 request = putRequestWithBody;
586 if (!basicAuth.isEmpty()) {
588 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
591 request.headers().set("Authorization", "Basic " + basicAuth);
596 if (digestString != null) {
597 request.headers().set("Authorization", "Digest " + digestString);
601 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
602 .addListener(new ChannelFutureListener() {
605 public void operationComplete(@Nullable ChannelFuture future) {
606 if (future == null) {
609 if (future.isDone() && future.isSuccess()) {
610 Channel ch = future.channel();
611 openChannels.add(ch);
615 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
618 openChannel(ch, httpRequestURL);
619 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
620 commonHandler.setURL(httpRequestURLFull);
621 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
622 authHandler.setURL(httpMethod, httpRequestURL);
624 switch (thing.getThingTypeUID().getId()) {
626 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
627 amcrestHandler.setURL(httpRequestURL);
630 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
631 instarHandler.setURL(httpRequestURL);
634 ch.writeAndFlush(request);
635 } else { // an error occured
636 cameraCommunicationError(
637 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
643 public void processSnapshot(byte[] incommingSnapshot) {
644 lockCurrentSnapshot.lock();
646 currentSnapshot = incommingSnapshot;
647 if (cameraConfig.getGifPreroll() > 0) {
648 fifoSnapshotBuffer.add(incommingSnapshot);
649 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
650 fifoSnapshotBuffer.removeFirst();
654 lockCurrentSnapshot.unlock();
655 currentSnapshotTime = Instant.now();
658 if (updateImageChannel) {
659 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
660 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
661 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
662 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
663 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
664 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
665 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
669 public void startStreamServer() {
670 servlet = new CameraServlet(this, httpService);
671 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
672 + getThing().getUID().getId() + "/ipcamera.m3u8"));
673 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
674 + getThing().getUID().getId() + "/ipcamera.jpg"));
675 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
676 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
679 public void openCamerasStream() {
680 if (mjpegUri.isEmpty() || "ffmpeg".equals(mjpegUri)) {
681 setupFfmpegFormat(FFmpegFormat.MJPEG);
684 closeChannel(getTinyUrl(mjpegUri));
685 // Dahua cameras crash if you refresh (close and open) the stream without this delay.
686 mainEventLoopGroup.schedule(this::openMjpegStream, 300, TimeUnit.MILLISECONDS);
689 private void openMjpegStream() {
690 sendHttpGET(mjpegUri);
693 private void openChannel(Channel channel, String httpRequestURL) {
694 ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
695 if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
696 tracker.setChannel(channel);
699 channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
702 public void closeChannel(String url) {
703 ChannelTracking channelTracking = channelTrackingMap.get(url);
704 if (channelTracking != null) {
705 if (channelTracking.getChannel().isOpen()) {
706 channelTracking.getChannel().close();
713 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
714 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
717 @SuppressWarnings("PMD.CompareObjectsWithEquals")
718 private void cleanChannels() {
719 for (Channel channel : openChannels) {
720 boolean oldChannel = true;
721 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
722 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
723 channelTrackingMap.remove(channelTracking.getRequestUrl());
725 if (channelTracking.getChannel() == channel) {
726 logger.debug("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
736 public void storeHttpReply(String url, String content) {
737 ChannelTracking channelTracking = channelTrackingMap.get(url);
738 if (channelTracking != null) {
739 channelTracking.setReply(content);
743 private void storeSnapshots() {
745 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
746 lockCurrentSnapshot.lock();
748 for (byte[] foo : fifoSnapshotBuffer) {
749 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
752 OutputStream fos = new FileOutputStream(file);
755 } catch (FileNotFoundException e) {
756 logger.warn("FileNotFoundException {}", e.getMessage());
757 } catch (IOException e) {
758 logger.warn("IOException {}", e.getMessage());
762 lockCurrentSnapshot.unlock();
766 public void setupFfmpegFormat(FFmpegFormat format) {
767 String inputOptions = cameraConfig.getFfmpegInputOptions();
768 if (cameraConfig.getFfmpegOutput().isEmpty()) {
769 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
772 if (rtspUri.isEmpty()) {
773 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
776 if (cameraConfig.getFfmpegLocation().isEmpty()) {
777 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
780 if (rtspUri.toLowerCase().contains("rtsp")) {
781 if (inputOptions.isEmpty()) {
782 inputOptions = "-rtsp_transport tcp";
786 // Make sure the folder exists, if not create it.
787 new File(cameraConfig.getFfmpegOutput()).mkdirs();
790 if (ffmpegHLS == null) {
791 if (!inputOptions.isEmpty()) {
792 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
793 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
794 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
795 cameraConfig.getUser(), cameraConfig.getPassword());
797 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
798 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
799 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
800 cameraConfig.getPassword());
803 Ffmpeg localHLS = ffmpegHLS;
804 if (localHLS != null) {
805 localHLS.startConverting();
809 if (cameraConfig.getGifPreroll() > 0) {
810 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
811 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
812 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
813 + cameraConfig.getGifOutOptions(),
814 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
815 cameraConfig.getPassword());
817 if (!inputOptions.isEmpty()) {
818 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
820 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
822 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
823 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
824 cameraConfig.getUser(), cameraConfig.getPassword());
826 if (cameraConfig.getGifPreroll() > 0) {
829 Ffmpeg localGIF = ffmpegGIF;
830 if (localGIF != null) {
831 localGIF.startConverting();
832 if (gifHistory.isEmpty()) {
833 gifHistory = gifFilename;
834 } else if (!"ipcamera".equals(gifFilename)) {
835 gifHistory = gifFilename + "," + gifHistory;
836 if (gifHistoryLength > 49) {
837 int endIndex = gifHistory.lastIndexOf(",");
838 gifHistory = gifHistory.substring(0, endIndex);
841 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
845 if (!inputOptions.isEmpty()) {
846 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
848 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
850 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
851 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
852 cameraConfig.getUser(), cameraConfig.getPassword());
853 Ffmpeg localRecord = ffmpegRecord;
854 if (localRecord != null) {
855 localRecord.startConverting();
856 if (mp4History.isEmpty()) {
857 mp4History = mp4Filename;
858 } else if (!"ipcamera".equals(mp4Filename)) {
859 mp4History = mp4Filename + "," + mp4History;
860 if (mp4HistoryLength > 49) {
861 int endIndex = mp4History.lastIndexOf(",");
862 mp4History = mp4History.substring(0, endIndex);
866 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
869 Ffmpeg localAlarms = ffmpegRtspHelper;
870 if (localAlarms != null) {
871 localAlarms.stopConverting();
872 if (!ffmpegAudioAlarmEnabled && !ffmpegMotionAlarmEnabled) {
876 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
877 String filterOptions = "";
878 if (!ffmpegAudioAlarmEnabled) {
879 filterOptions = "-an";
881 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
883 if (!ffmpegMotionAlarmEnabled && !ffmpegSnapshotGeneration) {
884 filterOptions = filterOptions.concat(" -vn");
885 } else if (ffmpegMotionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
886 String usersMotionOptions = cameraConfig.getMotionOptions();
887 if (usersMotionOptions.startsWith("-")) {
888 // Need to put the users custom options first in the chain before the motion is detected
889 filterOptions += " " + usersMotionOptions + ",select='gte(scene,"
890 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
892 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
893 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
895 } else if (ffmpegMotionAlarmEnabled) {
896 filterOptions = filterOptions.concat(" -vf select='gte(scene,"
897 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print");
899 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
900 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
901 localAlarms = ffmpegRtspHelper;
902 if (localAlarms != null) {
903 localAlarms.startConverting();
907 if (ffmpegMjpeg == null) {
908 if (inputOptions.isEmpty()) {
909 inputOptions = "-hide_banner";
911 inputOptions += " -hide_banner";
913 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
914 cameraConfig.getMjpegOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
915 + getThing().getUID().getId() + "/ipcamera.jpg",
916 cameraConfig.getUser(), cameraConfig.getPassword());
918 Ffmpeg localMjpeg = ffmpegMjpeg;
919 if (localMjpeg != null) {
920 localMjpeg.startConverting();
924 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
925 if (ffmpegSnapshot == null) {
926 if (inputOptions.isEmpty()) {
928 inputOptions = "-threads 1 -skip_frame nokey -hide_banner";
930 inputOptions += " -threads 1 -skip_frame nokey -hide_banner";
932 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
933 cameraConfig.getSnapshotOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
934 + getThing().getUID().getId() + "/snapshot.jpg",
935 cameraConfig.getUser(), cameraConfig.getPassword());
937 Ffmpeg localSnaps = ffmpegSnapshot;
938 if (localSnaps != null) {
939 localSnaps.startConverting();
945 public void noMotionDetected(String thisAlarmsChannel) {
946 setChannelState(thisAlarmsChannel, OnOffType.OFF);
947 firstMotionAlarm = false;
948 motionAlarmUpdateSnapshot = false;
949 motionDetected = false;
950 if (streamingAutoFps) {
951 stopSnapshotPolling();
952 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
953 stopSnapshotPolling();
958 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
959 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
960 * tampering with the camera.
962 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
963 updateState(thisAlarmsChannel, state);
966 public void motionDetected(String thisAlarmsChannel) {
967 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
968 updateState(thisAlarmsChannel, OnOffType.ON);
969 motionDetected = true;
970 if (streamingAutoFps) {
971 startSnapshotPolling();
973 if (cameraConfig.getUpdateImageWhen().contains("2")) {
974 if (!firstMotionAlarm) {
975 if (!snapshotUri.isEmpty()) {
978 firstMotionAlarm = true;// reset back to false when the jpg arrives.
980 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
981 if (!snapshotPolling) {
982 startSnapshotPolling();
984 firstMotionAlarm = true;
985 motionAlarmUpdateSnapshot = true;
989 public void audioDetected() {
990 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
991 if (cameraConfig.getUpdateImageWhen().contains("3")) {
992 if (!firstAudioAlarm) {
993 if (!snapshotUri.isEmpty()) {
996 firstAudioAlarm = true;// reset back to false when the jpg arrives.
998 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
999 firstAudioAlarm = true;
1000 audioAlarmUpdateSnapshot = true;
1004 public void noAudioDetected() {
1005 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1006 firstAudioAlarm = false;
1007 audioAlarmUpdateSnapshot = false;
1010 public void recordMp4(String filename, int seconds) {
1011 mp4Filename = filename;
1012 mp4RecordTime = seconds;
1013 setupFfmpegFormat(FFmpegFormat.RECORD);
1014 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1017 public void recordGif(String filename, int seconds) {
1018 gifFilename = filename;
1019 gifRecordTime = seconds;
1020 if (cameraConfig.getGifPreroll() > 0) {
1021 snapCount = seconds;
1023 setupFfmpegFormat(FFmpegFormat.GIF);
1025 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1028 public String returnValueFromString(String rawString, String searchedString) {
1030 int index = rawString.indexOf(searchedString);
1031 if (index != -1) // -1 means "not found"
1033 result = rawString.substring(index + searchedString.length(), rawString.length());
1034 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1036 return result; // Did not find a carriage return.
1038 return result.substring(0, index);
1041 return ""; // Did not find the String we were searching for
1044 private void sendPTZRequest() {
1045 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1049 public void channelLinked(ChannelUID channelUID) {
1050 switch (channelUID.getId()) {
1051 case CHANNEL_MJPEG_URL:
1052 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1053 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
1055 case CHANNEL_HLS_URL:
1056 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1057 + getThing().getUID().getId() + "/ipcamera.m3u8"));
1059 case CHANNEL_IMAGE_URL:
1060 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1061 + getThing().getUID().getId() + "/ipcamera.jpg"));
1067 public void handleCommand(ChannelUID channelUID, Command command) {
1068 if (command instanceof RefreshType) {
1069 switch (channelUID.getId()) {
1071 if (onvifCamera.supportsPTZ()) {
1072 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1076 if (onvifCamera.supportsPTZ()) {
1077 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1081 if (onvifCamera.supportsPTZ()) {
1082 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1085 case CHANNEL_GOTO_PRESET:
1086 if (onvifCamera.supportsPTZ()) {
1087 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1091 } // caution "REFRESH" can still progress to brand Handlers below the else.
1093 switch (channelUID.getId()) {
1094 case CHANNEL_MP4_HISTORY_LENGTH:
1095 if (DecimalType.ZERO.equals(command)) {
1096 mp4HistoryLength = 0;
1098 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1101 case CHANNEL_GIF_HISTORY_LENGTH:
1102 if (DecimalType.ZERO.equals(command)) {
1103 gifHistoryLength = 0;
1105 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1108 case CHANNEL_FFMPEG_MOTION_CONTROL:
1109 if (OnOffType.ON.equals(command)) {
1110 ffmpegMotionAlarmEnabled = true;
1111 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1112 ffmpegMotionAlarmEnabled = false;
1113 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1114 } else if (command instanceof PercentType) {
1115 ffmpegMotionAlarmEnabled = true;
1116 motionThreshold = ((PercentType) command).toBigDecimal();
1118 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1120 case CHANNEL_START_STREAM:
1122 if (OnOffType.ON.equals(command)) {
1123 localHLS = ffmpegHLS;
1124 if (localHLS == null) {
1125 setupFfmpegFormat(FFmpegFormat.HLS);
1126 localHLS = ffmpegHLS;
1128 if (localHLS != null) {
1129 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1130 localHLS.startConverting();
1133 localHLS = ffmpegHLS;
1134 if (localHLS != null) {
1135 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1136 localHLS.setKeepAlive(1);
1140 case CHANNEL_EXTERNAL_MOTION:
1141 if (OnOffType.ON.equals(command)) {
1142 motionDetected(CHANNEL_EXTERNAL_MOTION);
1144 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1147 case CHANNEL_GOTO_PRESET:
1148 if (onvifCamera.supportsPTZ()) {
1149 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1152 case CHANNEL_POLL_IMAGE:
1153 if (OnOffType.ON.equals(command)) {
1154 if (snapshotUri.isEmpty()) {
1155 ffmpegSnapshotGeneration = true;
1156 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1157 updateImageChannel = false;
1159 updateImageChannel = true;
1160 updateSnapshot();// Allows this to change Image FPS on demand
1163 Ffmpeg localSnaps = ffmpegSnapshot;
1164 if (localSnaps != null) {
1165 localSnaps.stopConverting();
1166 ffmpegSnapshotGeneration = false;
1168 updateImageChannel = false;
1172 if (onvifCamera.supportsPTZ()) {
1173 if (command instanceof IncreaseDecreaseType) {
1174 if (command == IncreaseDecreaseType.INCREASE) {
1175 if (cameraConfig.getPtzContinuous()) {
1176 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1178 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1181 if (cameraConfig.getPtzContinuous()) {
1182 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1184 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1188 } else if (OnOffType.OFF.equals(command)) {
1189 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1192 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1193 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1197 if (onvifCamera.supportsPTZ()) {
1198 if (command instanceof IncreaseDecreaseType) {
1199 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1200 if (cameraConfig.getPtzContinuous()) {
1201 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1203 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1206 if (cameraConfig.getPtzContinuous()) {
1207 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1209 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1213 } else if (OnOffType.OFF.equals(command)) {
1214 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1217 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1218 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1222 if (onvifCamera.supportsPTZ()) {
1223 if (command instanceof IncreaseDecreaseType) {
1224 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1225 if (cameraConfig.getPtzContinuous()) {
1226 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1228 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1231 if (cameraConfig.getPtzContinuous()) {
1232 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1234 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1238 } else if (OnOffType.OFF.equals(command)) {
1239 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1242 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1243 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1248 // commands and refresh now get passed to brand handlers
1249 switch (thing.getThingTypeUID().getId()) {
1251 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1252 amcrestHandler.handleCommand(channelUID, command);
1253 if (lowPriorityRequests.isEmpty()) {
1254 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1258 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1259 dahuaHandler.handleCommand(channelUID, command);
1260 if (lowPriorityRequests.isEmpty()) {
1261 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1264 case DOORBIRD_THING:
1265 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1266 doorBirdHandler.handleCommand(channelUID, command);
1267 if (lowPriorityRequests.isEmpty()) {
1268 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1271 case HIKVISION_THING:
1272 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1273 hikvisionHandler.handleCommand(channelUID, command);
1276 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1277 cameraConfig.getPassword());
1278 foscamHandler.handleCommand(channelUID, command);
1279 if (lowPriorityRequests.isEmpty()) {
1280 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1284 InstarHandler instarHandler = new InstarHandler(getHandle());
1285 instarHandler.handleCommand(channelUID, command);
1286 if (lowPriorityRequests.isEmpty()) {
1287 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1291 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1292 defaultHandler.handleCommand(channelUID, command);
1293 if (lowPriorityRequests.isEmpty()) {
1294 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1300 public void setChannelState(String channelToUpdate, State valueOf) {
1301 updateState(channelToUpdate, valueOf);
1304 private void bringCameraOnline() {
1306 updateStatus(ThingStatus.ONLINE);
1307 groupTracker.listOfOnlineCameraHandlers.add(this);
1308 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1309 Future<?> localFuture = cameraConnectionJob;
1310 if (localFuture != null) {
1311 localFuture.cancel(false);
1312 cameraConnectionJob = null;
1314 if (!snapshotUri.isEmpty()) {
1315 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1316 snapshotPolling = true;
1317 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000,
1318 cameraConfig.getPollTime(), TimeUnit.MILLISECONDS);
1322 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1324 // auto restart mjpeg stream now camera is back online.
1325 CameraServlet localServlet = servlet;
1326 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1327 openCamerasStream();
1330 if (!rtspUri.isEmpty()) {
1331 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1333 if (updateImageChannel) {
1334 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1336 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1338 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1339 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1340 handle.cameraOnline(getThing().getUID().getId());
1345 void snapshotIsFfmpeg() {
1346 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1348 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1349 bringCameraOnline();
1350 if (!rtspUri.isEmpty()) {
1351 updateImageChannel = false;
1352 ffmpegSnapshotGeneration = true;
1353 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1354 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1356 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1360 void pollingCameraConnection() {
1362 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)
1363 || thing.getThingTypeUID().getId().equals(DOORBIRD_THING)) {
1364 if (rtspUri.isEmpty()) {
1365 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1367 if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1370 ffmpegSnapshotGeneration = false;
1375 if (cameraConfig.getOnvifPort() > 0 && !onvifCamera.isConnected()) {
1376 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1377 cameraConfig.getOnvifPort());
1378 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1380 if ("ffmpeg".equals(snapshotUri)) {
1382 } else if (!snapshotUri.isEmpty()) {
1383 ffmpegSnapshotGeneration = false;
1385 } else if (!rtspUri.isEmpty()) {
1388 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1389 "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.");
1393 public void cameraConfigError(String reason) {
1394 // wont try to reconnect again due to a config error being the cause.
1395 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1399 public void cameraCommunicationError(String reason) {
1400 // will try to reconnect again as camera may be rebooting.
1401 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1402 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1403 resetAndRetryConnecting();
1407 private boolean streamIsStopped(String url) {
1408 ChannelTracking channelTracking = channelTrackingMap.get(url);
1409 if (channelTracking != null) {
1410 if (channelTracking.getChannel().isActive()) {
1411 return false; // stream is running.
1414 return true; // Stream stopped or never started.
1417 void snapshotRunnable() {
1418 // Snapshot should be first to keep consistent time between shots
1420 if (snapCount > 0) {
1421 if (--snapCount == 0) {
1422 setupFfmpegFormat(FFmpegFormat.GIF);
1427 private void takeSnapshot() {
1428 sendHttpGET(snapshotUri);
1431 private void updateSnapshot() {
1432 lastSnapshotRequest = Instant.now();
1433 mainEventLoopGroup.schedule(this::takeSnapshot, 0, TimeUnit.MILLISECONDS);
1436 public byte[] getSnapshot() {
1438 // Single gray pixel JPG to keep streams open when the camera goes offline so they dont stop.
1439 return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
1440 0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43,
1441 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
1442 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09,
1443 0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d,
1444 0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10,
1445 0x10, (byte) 0xff, (byte) 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
1446 (byte) 0xff, (byte) 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, (byte) 0xff, (byte) 0xda, 0x00, 0x08,
1447 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, (byte) 0xd2, (byte) 0xcf, 0x20, (byte) 0xff, (byte) 0xd9 };
1449 // Most cameras will return a 503 busy error if snapshot is faster than 1 second
1450 long lastUpdatedMs = Duration.between(lastSnapshotRequest, Instant.now()).toMillis();
1451 if (!snapshotPolling && !ffmpegSnapshotGeneration && lastUpdatedMs >= cameraConfig.getPollTime()) {
1454 lockCurrentSnapshot.lock();
1456 return currentSnapshot;
1458 lockCurrentSnapshot.unlock();
1462 public void stopSnapshotPolling() {
1463 Future<?> localFuture;
1464 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1465 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1466 snapshotPolling = false;
1467 localFuture = snapshotJob;
1468 if (localFuture != null) {
1469 localFuture.cancel(true);
1471 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1472 snapshotPolling = false;
1473 localFuture = snapshotJob;
1474 if (localFuture != null) {
1475 localFuture.cancel(true);
1480 public void startSnapshotPolling() {
1481 if (snapshotPolling || ffmpegSnapshotGeneration) {
1482 return; // Already polling or creating with FFmpeg from RTSP
1484 if (streamingSnapshotMjpeg || streamingAutoFps || cameraConfig.getUpdateImageWhen().contains("4")) {
1485 snapshotPolling = true;
1486 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 0, cameraConfig.getPollTime(),
1487 TimeUnit.MILLISECONDS);
1492 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep alarm
1493 * streams open and more.
1496 void pollCameraRunnable() {
1497 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1498 if (!lowPriorityRequests.isEmpty()) {
1499 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1500 lowPriorityCounter = 0;
1502 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1504 // what needs to be done every poll//
1505 switch (thing.getThingTypeUID().getId()) {
1507 if (!snapshotPolling) {
1508 checkCameraConnection();
1512 if (!snapshotPolling) {
1513 checkCameraConnection();
1515 if (!onvifCamera.isConnected()) {
1516 onvifCamera.connect(true);
1520 if (!snapshotPolling) {
1521 checkCameraConnection();
1523 noMotionDetected(CHANNEL_MOTION_ALARM);
1524 noMotionDetected(CHANNEL_PIR_ALARM);
1525 noMotionDetected(CHANNEL_HUMAN_ALARM);
1526 noMotionDetected(CHANNEL_CAR_ALARM);
1527 noMotionDetected(CHANNEL_ANIMAL_ALARM);
1530 case HIKVISION_THING:
1531 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1532 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1533 cameraConfig.getIp());
1534 sendHttpGET("/ISAPI/Event/notification/alertStream");
1538 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1539 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1542 if (!snapshotPolling) {
1543 checkCameraConnection();
1545 // Check for alarms, channel for NVRs appears not to work at filtering.
1546 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1547 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1548 cameraConfig.getIp());
1549 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1552 case DOORBIRD_THING:
1553 if (!snapshotPolling) {
1554 checkCameraConnection();
1556 // Check for alarms, channel for NVRs appears not to work at filtering.
1557 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1558 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1559 cameraConfig.getIp());
1560 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1564 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1565 + cameraConfig.getPassword());
1568 Ffmpeg localFfmpeg = ffmpegHLS;
1569 if (localFfmpeg != null) {
1570 localFfmpeg.checkKeepAlive();
1572 if (ffmpegMotionAlarmEnabled || ffmpegAudioAlarmEnabled) {
1573 localFfmpeg = ffmpegRtspHelper;
1574 if (localFfmpeg == null || !localFfmpeg.getIsAlive()) {
1575 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1578 // check if the thread has frozen due to camera doing a soft reboot
1579 localFfmpeg = ffmpegMjpeg;
1580 if (localFfmpeg != null && !localFfmpeg.getIsAlive()) {
1581 logger.debug("MJPEG was not being produced by FFmpeg when it should have been, restarting FFmpeg.");
1582 setupFfmpegFormat(FFmpegFormat.MJPEG);
1584 if (openChannels.size() > 10) {
1585 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1591 public void initialize() {
1592 cameraConfig = getConfigAs(CameraConfig.class);
1593 threadPool = Executors.newScheduledThreadPool(2);
1594 mainEventLoopGroup = new NioEventLoopGroup(3);
1595 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1596 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1597 rtspUri = cameraConfig.getFfmpegInput();
1598 if (cameraConfig.getFfmpegOutput().isEmpty()) {
1600 .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1602 // Known cameras will connect quicker if we skip ONVIF questions.
1603 switch (thing.getThingTypeUID().getId()) {
1606 if (mjpegUri.isEmpty()) {
1607 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1609 if (snapshotUri.isEmpty()) {
1610 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1613 case DOORBIRD_THING:
1614 if (mjpegUri.isEmpty()) {
1615 mjpegUri = "/bha-api/video.cgi";
1617 if (snapshotUri.isEmpty()) {
1618 snapshotUri = "/bha-api/image.cgi";
1622 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1623 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1624 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1625 if (mjpegUri.isEmpty()) {
1626 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1627 + cameraConfig.getPassword();
1629 if (snapshotUri.isEmpty()) {
1630 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1631 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1634 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1635 if (mjpegUri.isEmpty()) {
1636 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1638 if (snapshotUri.isEmpty()) {
1639 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1641 if (lowPriorityRequests.isEmpty()) {
1642 lowPriorityRequests.add("/ISAPI/System/IO/inputs/" + cameraConfig.getNvrChannel() + "/status");
1646 if (snapshotUri.isEmpty()) {
1647 snapshotUri = "/tmpfs/snap.jpg";
1649 if (mjpegUri.isEmpty()) {
1650 mjpegUri = "/mjpegstream.cgi?-chn=12";
1652 // Newer Instar cameras use this to setup the Alarm Server, plus it is used to work out which API is
1653 // implemented based on the response to these two requests.
1655 "/param.cgi?cmd=setasaction&-server=1&enable=1&-interval=1&cmd=setasattr&-as_index=1&-as_server="
1656 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1657 + getThing().getUID().getId()
1658 + "/instar&-as_ssl=0&-as_insecure=0&-as_mode=0&-as_activequery=1&-as_auth=0&-as_query1=0&-as_query2=0&-as_query3=0&-as_query4=0&-as_query5=0");
1659 // Older Instar cameras use this to setup the Alarm Server
1661 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
1662 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1663 + getThing().getUID().getId()
1664 + "/instar&-as_ssl=0&-as_mode=1&-as_activequery=1&-as_auth=0&-as_query1=0&-as_query2=0&-as_query3=0&-as_query4=0&-as_query5=0");
1667 // for poll times 9 seconds and above don't display a warning about the Image channel.
1668 if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1670 "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.");
1672 // ONVIF and Instar event handling need the server started before connecting.
1673 startStreamServer();
1677 private void tryConnecting() {
1678 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)
1679 && !thing.getThingTypeUID().getId().equals(DOORBIRD_THING) && cameraConfig.getOnvifPort() > 0) {
1680 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1681 cameraConfig.getUser(), cameraConfig.getPassword());
1682 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1683 // Only use ONVIF events if it is not an API camera.
1684 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1686 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 8, TimeUnit.SECONDS);
1689 private void keepMjpegRunning() {
1690 CameraServlet localServlet = servlet;
1691 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1692 if (!mjpegUri.isEmpty() && !"ffmpeg".equals(mjpegUri)) {
1693 localServlet.openStreams.queueFrame(("--" + localServlet.openStreams.boundary + "\r\n\r\n").getBytes());
1695 localServlet.openStreams.queueFrame(getSnapshot());
1699 // What the camera needs to re-connect if the initialize() is not called.
1700 private void resetAndRetryConnecting() {
1705 private void offline() {
1707 snapshotPolling = false;
1708 Future<?> localFuture = pollCameraJob;
1709 if (localFuture != null) {
1710 localFuture.cancel(true);
1713 localFuture = snapshotJob;
1714 if (localFuture != null) {
1715 localFuture.cancel(true);
1718 localFuture = cameraConnectionJob;
1719 if (localFuture != null) {
1720 localFuture.cancel(true);
1723 Ffmpeg localFfmpeg = ffmpegHLS;
1724 if (localFfmpeg != null) {
1725 localFfmpeg.stopConverting();
1728 localFfmpeg = ffmpegRecord;
1729 if (localFfmpeg != null) {
1730 localFfmpeg.stopConverting();
1731 ffmpegRecord = null;
1733 localFfmpeg = ffmpegGIF;
1734 if (localFfmpeg != null) {
1735 localFfmpeg.stopConverting();
1738 localFfmpeg = ffmpegRtspHelper;
1739 if (localFfmpeg != null) {
1740 localFfmpeg.stopConverting();
1741 ffmpegRtspHelper = null;
1743 localFfmpeg = ffmpegMjpeg;
1744 if (localFfmpeg != null) {
1745 localFfmpeg.stopConverting();
1748 localFfmpeg = ffmpegSnapshot;
1749 if (localFfmpeg != null) {
1750 localFfmpeg.stopConverting();
1751 ffmpegSnapshot = null;
1753 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {// generic cameras do not have ONVIF support
1754 onvifCamera.disconnect();
1756 openChannels.close();
1760 public void dispose() {
1762 CameraServlet localServlet = servlet;
1763 if (localServlet != null) {
1764 localServlet.dispose();
1765 localServlet = null;
1767 threadPool.shutdown();
1768 // inform all group handlers that this camera has gone offline
1769 groupTracker.listOfOnlineCameraHandlers.remove(this);
1770 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1771 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1772 handle.cameraOffline(this);
1774 basicAuth = ""; // clear out stored Password hash
1775 useDigestAuth = false;
1776 mainEventLoopGroup.shutdownGracefully();
1777 mainBootstrap = null;
1778 channelTrackingMap.clear();
1781 public String getWhiteList() {
1782 return cameraConfig.getIpWhitelist();
1786 public Collection<Class<? extends ThingHandlerService>> getServices() {
1787 return Collections.singleton(IpCameraActions.class);