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.nio.charset.StandardCharsets;
27 import java.time.Duration;
28 import java.time.Instant;
29 import java.util.ArrayList;
30 import java.util.Collection;
31 import java.util.LinkedList;
32 import java.util.List;
35 import java.util.concurrent.ConcurrentHashMap;
36 import java.util.concurrent.Executors;
37 import java.util.concurrent.Future;
38 import java.util.concurrent.ScheduledExecutorService;
39 import java.util.concurrent.ScheduledFuture;
40 import java.util.concurrent.TimeUnit;
41 import java.util.concurrent.locks.ReentrantLock;
43 import org.eclipse.jdt.annotation.NonNullByDefault;
44 import org.eclipse.jdt.annotation.Nullable;
45 import org.openhab.binding.ipcamera.internal.AmcrestHandler;
46 import org.openhab.binding.ipcamera.internal.CameraConfig;
47 import org.openhab.binding.ipcamera.internal.ChannelTracking;
48 import org.openhab.binding.ipcamera.internal.DahuaHandler;
49 import org.openhab.binding.ipcamera.internal.DoorBirdHandler;
50 import org.openhab.binding.ipcamera.internal.Ffmpeg;
51 import org.openhab.binding.ipcamera.internal.FoscamHandler;
52 import org.openhab.binding.ipcamera.internal.GroupTracker;
53 import org.openhab.binding.ipcamera.internal.Helper;
54 import org.openhab.binding.ipcamera.internal.HikvisionHandler;
55 import org.openhab.binding.ipcamera.internal.HttpOnlyHandler;
56 import org.openhab.binding.ipcamera.internal.InstarHandler;
57 import org.openhab.binding.ipcamera.internal.IpCameraActions;
58 import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
59 import org.openhab.binding.ipcamera.internal.IpCameraDynamicStateDescriptionProvider;
60 import org.openhab.binding.ipcamera.internal.MyNettyAuthHandler;
61 import org.openhab.binding.ipcamera.internal.ReolinkHandler;
62 import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection;
63 import org.openhab.binding.ipcamera.internal.servlet.CameraServlet;
64 import org.openhab.core.OpenHAB;
65 import org.openhab.core.library.types.DecimalType;
66 import org.openhab.core.library.types.IncreaseDecreaseType;
67 import org.openhab.core.library.types.OnOffType;
68 import org.openhab.core.library.types.PercentType;
69 import org.openhab.core.library.types.RawType;
70 import org.openhab.core.library.types.StringType;
71 import org.openhab.core.thing.ChannelUID;
72 import org.openhab.core.thing.Thing;
73 import org.openhab.core.thing.ThingStatus;
74 import org.openhab.core.thing.ThingStatusDetail;
75 import org.openhab.core.thing.binding.BaseThingHandler;
76 import org.openhab.core.thing.binding.ThingHandlerService;
77 import org.openhab.core.thing.binding.builder.ThingBuilder;
78 import org.openhab.core.types.Command;
79 import org.openhab.core.types.RefreshType;
80 import org.openhab.core.types.State;
81 import org.osgi.framework.FrameworkUtil;
82 import org.osgi.service.http.HttpService;
83 import org.slf4j.Logger;
84 import org.slf4j.LoggerFactory;
86 import io.netty.bootstrap.Bootstrap;
87 import io.netty.buffer.ByteBuf;
88 import io.netty.buffer.Unpooled;
89 import io.netty.channel.Channel;
90 import io.netty.channel.ChannelDuplexHandler;
91 import io.netty.channel.ChannelFuture;
92 import io.netty.channel.ChannelFutureListener;
93 import io.netty.channel.ChannelHandlerContext;
94 import io.netty.channel.ChannelInitializer;
95 import io.netty.channel.ChannelOption;
96 import io.netty.channel.EventLoopGroup;
97 import io.netty.channel.group.ChannelGroup;
98 import io.netty.channel.group.DefaultChannelGroup;
99 import io.netty.channel.nio.NioEventLoopGroup;
100 import io.netty.channel.socket.SocketChannel;
101 import io.netty.channel.socket.nio.NioSocketChannel;
102 import io.netty.handler.codec.base64.Base64;
103 import io.netty.handler.codec.http.DefaultFullHttpRequest;
104 import io.netty.handler.codec.http.FullHttpRequest;
105 import io.netty.handler.codec.http.HttpClientCodec;
106 import io.netty.handler.codec.http.HttpContent;
107 import io.netty.handler.codec.http.HttpHeaderValues;
108 import io.netty.handler.codec.http.HttpMessage;
109 import io.netty.handler.codec.http.HttpMethod;
110 import io.netty.handler.codec.http.HttpResponse;
111 import io.netty.handler.codec.http.HttpVersion;
112 import io.netty.handler.codec.http.LastHttpContent;
113 import io.netty.handler.timeout.IdleState;
114 import io.netty.handler.timeout.IdleStateEvent;
115 import io.netty.handler.timeout.IdleStateHandler;
116 import io.netty.util.CharsetUtil;
117 import io.netty.util.ReferenceCountUtil;
118 import io.netty.util.concurrent.GlobalEventExecutor;
121 * The {@link IpCameraHandler} is responsible for handling commands, which are
122 * sent to one of the channels.
124 * @author Matthew Skinner - Initial contribution
127 public class IpCameraHandler extends BaseThingHandler {
128 public final Logger logger = LoggerFactory.getLogger(getClass());
129 public final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
130 private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2);
131 private GroupTracker groupTracker;
132 public CameraConfig cameraConfig = new CameraConfig();
134 // ChannelGroup is thread safe
135 public final ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
136 private final HttpService httpService;
137 private @Nullable CameraServlet servlet;
138 public String mjpegContentType = "";
139 public @Nullable Ffmpeg ffmpegHLS = null;
140 public @Nullable Ffmpeg ffmpegRecord = null;
141 public @Nullable Ffmpeg ffmpegGIF = null;
142 public @Nullable Ffmpeg ffmpegRtspHelper = null;
143 public @Nullable Ffmpeg ffmpegMjpeg = null;
144 public @Nullable Ffmpeg ffmpegSnapshot = null;
145 public boolean streamingAutoFps = false;
146 public boolean motionDetected = false;
147 public Instant lastSnapshotRequest = Instant.now();
148 public Instant currentSnapshotTime = Instant.now();
149 private @Nullable ScheduledFuture<?> cameraConnectionJob = null;
150 private @Nullable ScheduledFuture<?> pollCameraJob = null;
151 private @Nullable ScheduledFuture<?> snapshotJob = null;
152 private @Nullable ScheduledFuture<?> authenticationJob = null;
153 private @Nullable Bootstrap mainBootstrap;
154 private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup(1);
155 private FullHttpRequest putRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT, "");
156 private FullHttpRequest postRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "");
157 private String gifFilename = "ipcamera";
158 private String gifHistory = "";
159 private String mp4History = "";
160 public int gifHistoryLength;
161 public int mp4HistoryLength;
162 private String mp4Filename = "ipcamera";
163 private int mp4RecordTime;
164 private int gifRecordTime = 5;
165 private LinkedList<byte[]> fifoSnapshotBuffer = new LinkedList<byte[]>();
166 private int snapCount;
167 private boolean updateImageChannel = false;
168 private byte lowPriorityCounter = 0;
169 public String hostIp;
170 public Map<String, ChannelTracking> channelTrackingMap = new ConcurrentHashMap<>();
171 public List<String> lowPriorityRequests = new ArrayList<>(0);
173 // basicAuth MUST remain private as it holds the cameraConfig.getPassword()
174 private String basicAuth = "";
175 public String reolinkAuth = "&token=null";
176 public boolean useBasicAuth = false;
177 public boolean useDigestAuth = false;
178 public boolean newInstarApi = false;
179 public String snapshotUri = "";
180 public String mjpegUri = "";
181 private byte[] currentSnapshot = new byte[] { (byte) 0x00 };
182 public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
183 public String rtspUri = "";
184 public boolean audioAlarmUpdateSnapshot = false;
185 private boolean motionAlarmUpdateSnapshot = false;
186 private boolean isOnline = false; // Used so only 1 error is logged when a network issue occurs.
187 private boolean firstAudioAlarm = false;
188 private boolean firstMotionAlarm = false;
189 public BigDecimal motionThreshold = BigDecimal.ZERO;
190 public int audioThreshold = 35;
191 public boolean streamingSnapshotMjpeg = false;
192 public boolean ffmpegMotionAlarmEnabled = false;
193 public boolean ffmpegAudioAlarmEnabled = false;
194 public boolean ffmpegSnapshotGeneration = false;
195 public boolean snapshotPolling = false;
196 public OnvifConnection onvifCamera = new OnvifConnection(this, "", "", "");
198 // These methods handle the response from all camera brands, nothing specific to 1 brand.
199 private class CommonCameraHandler extends ChannelDuplexHandler {
200 private int bytesToRecieve = 0;
201 private int bytesAlreadyRecieved = 0;
202 private byte[] incomingJpeg = new byte[0];
203 private String incomingMessage = "";
204 private String contentType = "empty";
205 private String boundary = "";
206 private Object reply = new Object();
207 private String requestUrl = "";
208 private boolean isChunked = false;
210 public void setURL(String url) {
215 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
216 if (msg == null || ctx == null) {
220 if (msg instanceof HttpResponse response) {
221 if (response.status().code() == 200) {
222 if (!response.headers().isEmpty()) {
223 for (String name : response.headers().names()) {
224 // Some cameras use first letter uppercase and others dont.
225 switch (name.toLowerCase()) { // Possible localization issues doing this
227 contentType = response.headers().getAsString(name);
229 case "content-length":
230 bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
232 case "transfer-encoding":
233 if (response.headers().getAsString(name).contains("chunked")) {
239 if (contentType.contains("multipart")) {
240 boundary = Helper.searchString(contentType, "boundary=");
241 if (mjpegUri.equals(requestUrl)) {
242 if (msg instanceof HttpMessage) {
243 // very start of stream only
244 mjpegContentType = contentType;
245 CameraServlet localServlet = servlet;
246 if (localServlet != null) {
247 logger.debug("Setting Content-Type to: {}", contentType);
248 localServlet.openStreams.updateContentType(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];
261 // Non 200 OK replies are logged and handled in pipeline by MyNettyAuthHandler.java
265 if (msg instanceof HttpContent content) {
266 if (mjpegUri.equals(requestUrl) && !(content instanceof LastHttpContent)) {
267 // multiple MJPEG stream packets come back as this.
268 byte[] chunkedFrame = new byte[content.content().readableBytes()];
269 content.content().getBytes(content.content().readerIndex(), chunkedFrame);
270 CameraServlet localServlet = servlet;
271 if (localServlet != null) {
272 localServlet.openStreams.queueFrame(chunkedFrame);
275 // Found some cameras use Content-Type: image/jpg instead of image/jpeg
276 if (contentType.contains("image/jp")) {
277 for (int i = 0; i < content.content().capacity(); i++) {
278 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
280 if (content instanceof LastHttpContent) {
281 processSnapshot(incomingJpeg);
284 } else { // incomingMessage that is not an IMAGE
285 if (incomingMessage.isEmpty()) {
286 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
288 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
290 bytesAlreadyRecieved = incomingMessage.length();
291 if (content instanceof LastHttpContent) {
292 // If it is not an image send it on to the next handler//
293 if (bytesAlreadyRecieved != 0) {
294 reply = incomingMessage;
295 super.channelRead(ctx, reply);
298 // Alarm Streams never have a LastHttpContent as they always stay open//
299 else if (contentType.contains("multipart")) {
300 int beginIndex, endIndex;
301 if (bytesToRecieve == 0) {
302 beginIndex = incomingMessage.indexOf("Content-Length:");
303 if (beginIndex != -1) {
304 endIndex = incomingMessage.indexOf("\r\n", beginIndex);
305 if (endIndex != -1) {
306 bytesToRecieve = Integer.parseInt(
307 incomingMessage.substring(beginIndex + 15, endIndex).strip());
311 // --boundary and headers are not included in the Content-Length value
312 if (bytesAlreadyRecieved > bytesToRecieve) {
313 // Check if message has a second --boundary
314 endIndex = incomingMessage.indexOf("--" + boundary, bytesToRecieve);
315 if (endIndex == -1) {
316 reply = incomingMessage;
317 incomingMessage = "";
319 bytesAlreadyRecieved = 0;
321 reply = incomingMessage.substring(0, endIndex);
322 incomingMessage = incomingMessage.substring(endIndex, incomingMessage.length());
323 bytesToRecieve = 0;// Triggers search next time for Content-Length:
324 bytesAlreadyRecieved = incomingMessage.length() - endIndex;
326 super.channelRead(ctx, reply);
329 // Foscam needs this as will other cameras with chunks//
330 if (isChunked && bytesAlreadyRecieved != 0) {
331 reply = incomingMessage;
335 } else { // msg is not HttpContent
336 // Foscam cameras need this
337 if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
338 reply = incomingMessage;
339 logger.trace("Packet back from camera is {}", incomingMessage);
340 super.channelRead(ctx, reply);
344 ReferenceCountUtil.release(msg);
349 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
350 if (cause == null || ctx == null) {
353 if (cause instanceof ArrayIndexOutOfBoundsException) {
354 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
357 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
364 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
368 if (evt instanceof IdleStateEvent e) {
369 // If camera does not use the channel for X amount of time it will close.
370 if (e.state() == IdleState.READER_IDLE) {
371 String urlToKeepOpen = "";
372 switch (thing.getThingTypeUID().getId()) {
374 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
377 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
380 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
381 if (channelTracking != null) {
382 if (channelTracking.getChannel().equals(ctx.channel())) {
383 return; // don't auto close this as it is for the alarms.
386 logger.debug("Closing an idle channel for camera: {}", cameraConfig.getIp());
393 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
394 IpCameraDynamicStateDescriptionProvider stateDescriptionProvider, HttpService httpService) {
396 this.stateDescriptionProvider = stateDescriptionProvider;
397 if (ipAddress != null) {
400 hostIp = Helper.getLocalIpAddress();
402 this.groupTracker = groupTracker;
403 this.httpService = httpService;
406 private IpCameraHandler getHandle() {
410 // false clears the stored user/pass hash, true creates the hash
411 public boolean setBasicAuth(boolean useBasic) {
413 logger.debug("Clearing out the stored BASIC auth now.");
416 } else if (!basicAuth.isEmpty()) {
417 // If the binding sends multiple requests before basicAuth was set, this may trigger falsely.
418 logger.warn("Camera is reporting your username and/or password is wrong.");
421 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
422 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
423 ByteBuf byteBuf = null;
425 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
426 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
428 if (byteBuf != null) {
434 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
439 private String getCorrectUrlFormat(String longUrl) {
440 String temp = longUrl;
443 if (longUrl.isEmpty() || "ffmpeg".equals(longUrl)) {
448 url = new URL(longUrl);
449 int port = url.getPort();
451 if (url.getQuery() == null) {
452 temp = url.getPath();
454 temp = url.getPath() + "?" + url.getQuery();
457 if (url.getQuery() == null) {
458 temp = ":" + url.getPort() + url.getPath();
460 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
463 } catch (MalformedURLException e) {
464 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
469 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
470 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
471 sendHttpRequest("PUT", httpRequestURL, null);
474 public void sendHttpPOST(String httpPostURL, String content) {
475 FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, httpPostURL);
476 request.headers().set("Host", cameraConfig.getIp());
477 request.headers().add("Content-Type", "application/json");
478 request.headers().add("User-Agent",
479 "openHAB/" + FrameworkUtil.getBundle(this.getClass()).getVersion().toString());
480 request.headers().add("Accept", "*/*");
481 ByteBuf bbuf = Unpooled.copiedBuffer(content, StandardCharsets.UTF_8);
482 request.headers().set("Content-Length", bbuf.readableBytes());
483 request.content().clear().writeBytes(bbuf);
484 postRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
485 sendHttpRequest("POST", httpPostURL, null);
488 public void sendHttpPOST(String httpRequestURL) {
489 sendHttpRequest("POST", httpRequestURL, null);
492 public void sendHttpGET(String httpRequestURL) {
493 sendHttpRequest("GET", httpRequestURL, null);
496 public int getPortFromShortenedUrl(String httpRequestURL) {
497 if (httpRequestURL.startsWith(":")) {
498 int end = httpRequestURL.indexOf("/");
499 return Integer.parseInt(httpRequestURL.substring(1, end));
501 return cameraConfig.getPort();
504 public String getTinyUrl(String httpRequestURL) {
505 if (httpRequestURL.startsWith(":")) {
506 int beginIndex = httpRequestURL.indexOf("/");
507 return httpRequestURL.substring(beginIndex);
509 return httpRequestURL;
512 private void checkCameraConnection() {
513 if (snapshotPolling) { // Currently polling a real URL for snapshots, so camera must be online.
515 } else if (ffmpegSnapshotGeneration) { // Use RTSP stream creating snapshots to know camera is online.
516 Ffmpeg localSnapshot = ffmpegSnapshot;
517 if (localSnapshot != null && !localSnapshot.isAlive()) {
518 cameraCommunicationError("FFmpeg Snapshots Stopped: Check that your camera can be reached.");
521 return; // ffmpeg snapshot stream is still alive
524 // if ONVIF cam also use connection state which is updated by regular messages to camera
525 if (thing.getThingTypeUID().getId().equals(ONVIF_THING) && snapshotUri.isEmpty() && onvifCamera.isConnected()) {
529 // Open a HTTP connection without sending any requests as we do not need a snapshot.
530 Bootstrap localBootstrap = mainBootstrap;
531 if (localBootstrap != null) {
532 ChannelFuture chFuture = localBootstrap
533 .connect(new InetSocketAddress(cameraConfig.getIp(), cameraConfig.getPort()));
534 if (chFuture.awaitUninterruptibly(500)) {
535 chFuture.channel().close();
539 cameraCommunicationError(
540 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
543 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
544 // The authHandler will generate a digest string and re-send using this same function when needed.
545 @SuppressWarnings("null")
546 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
547 int port = getPortFromShortenedUrl(httpRequestURLFull);
548 String httpRequestURL = getTinyUrl(httpRequestURLFull);
550 if (mainBootstrap == null) {
551 mainBootstrap = new Bootstrap();
552 mainBootstrap.group(mainEventLoopGroup);
553 mainBootstrap.channel(NioSocketChannel.class);
554 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
555 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
556 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
557 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
558 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
559 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
562 public void initChannel(SocketChannel socketChannel) throws Exception {
563 // HIK Alarm stream needs > 9sec idle to stop stream closing
564 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
565 socketChannel.pipeline().addLast(new HttpClientCodec());
566 socketChannel.pipeline().addLast(AUTH_HANDLER,
567 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
568 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
570 switch (thing.getThingTypeUID().getId()) {
572 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
575 socketChannel.pipeline()
576 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
579 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
582 socketChannel.pipeline().addLast(
583 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
585 case HIKVISION_THING:
586 socketChannel.pipeline()
587 .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
590 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
593 socketChannel.pipeline().addLast(REOLINK_HANDLER, new ReolinkHandler(getHandle()));
596 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
603 FullHttpRequest request;
604 if ("GET".equals(httpMethod) || (useDigestAuth && digestString == null)) {
605 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
606 request.headers().set("Host", cameraConfig.getIp() + ":" + port);
607 request.headers().set("Connection", HttpHeaderValues.CLOSE);
608 } else if ("PUT".equals(httpMethod)) {
609 request = putRequestWithBody;
611 request = postRequestWithBody;
614 if (!basicAuth.isEmpty()) {
616 logger.warn("Camera at IP: {} had both Basic and Digest set to be used", cameraConfig.getIp());
619 request.headers().set("Authorization", "Basic " + basicAuth);
624 if (digestString != null) {
625 request.headers().set("Authorization", "Digest " + digestString);
629 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
630 .addListener(new ChannelFutureListener() {
633 public void operationComplete(@Nullable ChannelFuture future) {
634 if (future == null) {
637 if (future.isDone() && future.isSuccess()) {
638 Channel ch = future.channel();
639 openChannels.add(ch);
643 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
646 openChannel(ch, httpRequestURL);
647 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
648 commonHandler.setURL(httpRequestURLFull);
649 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
650 authHandler.setURL(httpMethod, httpRequestURL);
652 switch (thing.getThingTypeUID().getId()) {
654 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
655 amcrestHandler.setURL(httpRequestURL);
658 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
659 instarHandler.setURL(httpRequestURL);
662 ReolinkHandler reolinkHandler = (ReolinkHandler) ch.pipeline().get(REOLINK_HANDLER);
663 reolinkHandler.setURL(httpRequestURL);
666 ch.writeAndFlush(request);
667 } else { // an error occurred
668 cameraCommunicationError(
669 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
675 public void processSnapshot(byte[] incommingSnapshot) {
676 lockCurrentSnapshot.lock();
678 currentSnapshot = incommingSnapshot;
679 if (cameraConfig.getGifPreroll() > 0) {
680 fifoSnapshotBuffer.add(incommingSnapshot);
681 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
682 fifoSnapshotBuffer.removeFirst();
686 lockCurrentSnapshot.unlock();
687 currentSnapshotTime = Instant.now();
690 if (updateImageChannel) {
691 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
692 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
693 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
694 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
695 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
696 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
697 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
701 public void startStreamServer() {
702 servlet = new CameraServlet(this, httpService);
703 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
704 + getThing().getUID().getId() + "/ipcamera.m3u8"));
705 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
706 + getThing().getUID().getId() + "/ipcamera.jpg"));
707 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
708 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
711 public void openCamerasStream() {
712 if (mjpegUri.isEmpty() || "ffmpeg".equals(mjpegUri)) {
713 setupFfmpegFormat(FFmpegFormat.MJPEG);
716 closeChannel(getTinyUrl(mjpegUri));
717 // Dahua cameras crash if you refresh (close and open) the stream without this delay.
718 mainEventLoopGroup.schedule(this::openMjpegStream, 300, TimeUnit.MILLISECONDS);
721 private void openMjpegStream() {
722 sendHttpGET(mjpegUri);
725 private void openChannel(Channel channel, String httpRequestURL) {
726 ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
727 if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
728 tracker.setChannel(channel);
731 channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
734 public void closeChannel(String url) {
735 ChannelTracking channelTracking = channelTrackingMap.get(url);
736 if (channelTracking != null) {
737 if (channelTracking.getChannel().isOpen()) {
738 channelTracking.getChannel().close();
745 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
746 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
749 private void cleanChannels() {
750 for (Channel channel : openChannels) {
751 boolean oldChannel = true;
752 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
753 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
754 channelTrackingMap.remove(channelTracking.getRequestUrl());
756 if (channelTracking.getChannel().equals(channel)) {
757 logger.debug("Open channel to camera is used for URL: {}", channelTracking.getRequestUrl());
767 public void storeHttpReply(String url, String content) {
768 ChannelTracking channelTracking = channelTrackingMap.get(url);
769 if (channelTracking != null) {
770 channelTracking.setReply(content);
774 private void storeSnapshots() {
776 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
777 lockCurrentSnapshot.lock();
779 for (byte[] foo : fifoSnapshotBuffer) {
780 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
783 OutputStream fos = new FileOutputStream(file);
786 } catch (FileNotFoundException e) {
787 logger.warn("FileNotFoundException {}", e.getMessage());
788 } catch (IOException e) {
789 logger.warn("IOException {}", e.getMessage());
793 lockCurrentSnapshot.unlock();
797 public void setupFfmpegFormat(FFmpegFormat format) {
798 String inputOptions = cameraConfig.getFfmpegInputOptions();
799 if (cameraConfig.getFfmpegOutput().isEmpty()) {
800 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
803 if (rtspUri.isEmpty()) {
804 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
807 if (cameraConfig.getFfmpegLocation().isEmpty()) {
808 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
811 if (rtspUri.toLowerCase().contains("rtsp")) {
812 if (inputOptions.isEmpty()) {
813 inputOptions = "-rtsp_transport tcp";
817 // Make sure the folder exists, if not create it.
818 new File(cameraConfig.getFfmpegOutput()).mkdirs();
821 if (ffmpegHLS == null) {
822 if (!inputOptions.isEmpty()) {
823 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
824 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
825 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
826 cameraConfig.getUser(), cameraConfig.getPassword());
828 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
829 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
830 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
831 cameraConfig.getPassword());
834 Ffmpeg localHLS = ffmpegHLS;
835 if (localHLS != null) {
836 localHLS.startConverting();
840 if (cameraConfig.getGifPreroll() > 0) {
841 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
842 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
843 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
844 + cameraConfig.getGifOutOptions(),
845 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
846 cameraConfig.getPassword());
848 if (!inputOptions.isEmpty()) {
849 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
851 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
853 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
854 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
855 cameraConfig.getUser(), cameraConfig.getPassword());
857 if (cameraConfig.getGifPreroll() > 0) {
860 Ffmpeg localGIF = ffmpegGIF;
861 if (localGIF != null) {
862 localGIF.startConverting();
863 if (gifHistory.isEmpty()) {
864 gifHistory = gifFilename;
865 } else if (!"ipcamera".equals(gifFilename)) {
866 gifHistory = gifFilename + "," + gifHistory;
867 if (gifHistoryLength > 49) {
868 int endIndex = gifHistory.lastIndexOf(",");
869 gifHistory = gifHistory.substring(0, endIndex);
872 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
876 if (!inputOptions.isEmpty()) {
877 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
879 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
881 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
882 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
883 cameraConfig.getUser(), cameraConfig.getPassword());
884 ffmpegRecord.startConverting();
885 if (mp4History.isEmpty()) {
886 mp4History = mp4Filename;
887 } else if (!"ipcamera".equals(mp4Filename)) {
888 mp4History = mp4Filename + "," + mp4History;
889 if (mp4HistoryLength > 49) {
890 int endIndex = mp4History.lastIndexOf(",");
891 mp4History = mp4History.substring(0, endIndex);
894 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
897 Ffmpeg localAlarms = ffmpegRtspHelper;
898 if (localAlarms != null) {
899 localAlarms.stopConverting();
900 if (!ffmpegAudioAlarmEnabled && !ffmpegMotionAlarmEnabled) {
904 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
905 String filterOptions = "";
906 if (!ffmpegAudioAlarmEnabled) {
907 filterOptions = "-an";
909 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
911 if (!ffmpegMotionAlarmEnabled && !ffmpegSnapshotGeneration) {
912 filterOptions = filterOptions.concat(" -vn");
913 } else if (ffmpegMotionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
914 String usersMotionOptions = cameraConfig.getMotionOptions();
915 if (usersMotionOptions.startsWith("-")) {
916 // Need to put the users custom options first in the chain before the motion is detected
917 filterOptions += " " + usersMotionOptions + ",select='gte(scene,"
918 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
920 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
921 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
923 } else if (ffmpegMotionAlarmEnabled) {
924 filterOptions = filterOptions.concat(" -vf select='gte(scene,"
925 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print");
927 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
928 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
929 ffmpegRtspHelper.startConverting();
932 if (ffmpegMjpeg == null) {
933 if (inputOptions.isEmpty()) {
934 inputOptions = "-hide_banner";
936 inputOptions += " -hide_banner";
938 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
939 cameraConfig.getMjpegOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
940 + getThing().getUID().getId() + "/ipcamera.jpg",
941 cameraConfig.getUser(), cameraConfig.getPassword());
943 Ffmpeg localMjpeg = ffmpegMjpeg;
944 if (localMjpeg != null) {
945 localMjpeg.startConverting();
949 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
950 if (ffmpegSnapshot == null) {
951 if (inputOptions.isEmpty()) {
953 inputOptions = "-threads 1 -skip_frame nokey -hide_banner";
955 inputOptions += " -threads 1 -skip_frame nokey -hide_banner";
957 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
958 cameraConfig.getSnapshotOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
959 + getThing().getUID().getId() + "/snapshot.jpg",
960 cameraConfig.getUser(), cameraConfig.getPassword());
962 Ffmpeg localSnaps = ffmpegSnapshot;
963 if (localSnaps != null) {
964 localSnaps.startConverting();
970 public void noMotionDetected(String thisAlarmsChannel) {
971 setChannelState(thisAlarmsChannel, OnOffType.OFF);
972 firstMotionAlarm = false;
973 motionAlarmUpdateSnapshot = false;
974 motionDetected = false;
975 if (streamingAutoFps) {
976 stopSnapshotPolling();
977 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
978 stopSnapshotPolling();
983 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
984 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
985 * tampering with the camera.
987 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
988 updateState(thisAlarmsChannel, state);
991 public void motionDetected(String thisAlarmsChannel) {
992 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
993 updateState(thisAlarmsChannel, OnOffType.ON);
994 motionDetected = true;
995 if (streamingAutoFps) {
996 startSnapshotPolling();
998 if (cameraConfig.getUpdateImageWhen().contains("2")) {
999 if (!firstMotionAlarm) {
1000 if (!snapshotUri.isEmpty()) {
1003 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1005 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1006 if (!snapshotPolling) {
1007 startSnapshotPolling();
1009 firstMotionAlarm = true;
1010 motionAlarmUpdateSnapshot = true;
1014 public void audioDetected() {
1015 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1016 if (cameraConfig.getUpdateImageWhen().contains("3")) {
1017 if (!firstAudioAlarm) {
1018 if (!snapshotUri.isEmpty()) {
1021 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1023 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1024 firstAudioAlarm = true;
1025 audioAlarmUpdateSnapshot = true;
1029 public void noAudioDetected() {
1030 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1031 firstAudioAlarm = false;
1032 audioAlarmUpdateSnapshot = false;
1035 public void recordMp4(String filename, int seconds) {
1036 mp4Filename = filename;
1037 mp4RecordTime = seconds;
1038 setupFfmpegFormat(FFmpegFormat.RECORD);
1039 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1042 public void recordGif(String filename, int seconds) {
1043 gifFilename = filename;
1044 gifRecordTime = seconds;
1045 if (cameraConfig.getGifPreroll() > 0) {
1046 snapCount = seconds;
1048 setupFfmpegFormat(FFmpegFormat.GIF);
1050 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1053 private void getReolinkToken() {
1054 sendHttpPOST("/api.cgi?cmd=Login",
1055 "[{\"cmd\":\"Login\", \"param\":{ \"User\":{ \"Version\": \"0\", \"userName\":\""
1056 + cameraConfig.getUser() + "\", \"password\":\"" + cameraConfig.getPassword() + "\"}}}]");
1059 public String returnValueFromString(String rawString, String searchedString) {
1061 int index = rawString.indexOf(searchedString);
1062 if (index != -1) // -1 means "not found"
1064 result = rawString.substring(index + searchedString.length(), rawString.length());
1065 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1067 return result; // Did not find a carriage return.
1069 return result.substring(0, index);
1072 return ""; // Did not find the String we were searching for
1075 private void sendPTZRequest() {
1076 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1080 public void channelLinked(ChannelUID channelUID) {
1081 switch (channelUID.getId()) {
1082 case CHANNEL_MJPEG_URL:
1083 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1084 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
1086 case CHANNEL_HLS_URL:
1087 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1088 + getThing().getUID().getId() + "/ipcamera.m3u8"));
1090 case CHANNEL_IMAGE_URL:
1091 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1092 + getThing().getUID().getId() + "/ipcamera.jpg"));
1097 public void removeChannels(List<org.openhab.core.thing.Channel> removeChannels) {
1098 if (!removeChannels.isEmpty()) {
1099 ThingBuilder thingBuilder = editThing();
1100 thingBuilder.withoutChannels(removeChannels);
1101 updateThing(thingBuilder.build());
1106 public void handleCommand(ChannelUID channelUID, Command command) {
1107 if (command instanceof RefreshType) {
1108 switch (channelUID.getId()) {
1110 if (onvifCamera.supportsPTZ()) {
1111 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1115 if (onvifCamera.supportsPTZ()) {
1116 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1120 if (onvifCamera.supportsPTZ()) {
1121 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1124 case CHANNEL_GOTO_PRESET:
1125 if (onvifCamera.supportsPTZ()) {
1126 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1130 } // caution "REFRESH" can still progress to brand Handlers below the else.
1132 switch (channelUID.getId()) {
1133 case CHANNEL_MP4_HISTORY_LENGTH:
1134 if (DecimalType.ZERO.equals(command)) {
1135 mp4HistoryLength = 0;
1137 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1140 case CHANNEL_GIF_HISTORY_LENGTH:
1141 if (DecimalType.ZERO.equals(command)) {
1142 gifHistoryLength = 0;
1144 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1147 case CHANNEL_FFMPEG_MOTION_CONTROL:
1148 if (OnOffType.ON.equals(command)) {
1149 ffmpegMotionAlarmEnabled = true;
1150 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1151 ffmpegMotionAlarmEnabled = false;
1152 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1153 } else if (command instanceof PercentType percentCommand) {
1154 ffmpegMotionAlarmEnabled = true;
1155 motionThreshold = percentCommand.toBigDecimal();
1157 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1159 case CHANNEL_START_STREAM:
1161 if (OnOffType.ON.equals(command)) {
1162 localHLS = ffmpegHLS;
1163 if (localHLS == null) {
1164 setupFfmpegFormat(FFmpegFormat.HLS);
1165 localHLS = ffmpegHLS;
1167 if (localHLS != null) {
1168 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1169 localHLS.startConverting();
1172 localHLS = ffmpegHLS;
1173 if (localHLS != null) {
1174 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1175 localHLS.setKeepAlive(1);
1179 case CHANNEL_EXTERNAL_MOTION:
1180 if (OnOffType.ON.equals(command)) {
1181 motionDetected(CHANNEL_EXTERNAL_MOTION);
1183 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1186 case CHANNEL_GOTO_PRESET:
1187 if (onvifCamera.supportsPTZ()) {
1188 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1191 case CHANNEL_POLL_IMAGE:
1192 if (OnOffType.ON.equals(command)) {
1193 if (snapshotUri.isEmpty()) {
1194 ffmpegSnapshotGeneration = true;
1195 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1196 updateImageChannel = false;
1198 updateImageChannel = true;
1199 updateSnapshot();// Allows this to change Image FPS on demand
1202 Ffmpeg localSnaps = ffmpegSnapshot;
1203 if (localSnaps != null) {
1204 localSnaps.stopConverting();
1205 ffmpegSnapshotGeneration = false;
1207 updateImageChannel = false;
1211 if (onvifCamera.supportsPTZ()) {
1212 if (command instanceof IncreaseDecreaseType) {
1213 if (command == IncreaseDecreaseType.INCREASE) {
1214 if (cameraConfig.getPtzContinuous()) {
1215 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1217 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1220 if (cameraConfig.getPtzContinuous()) {
1221 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1223 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1227 } else if (OnOffType.OFF.equals(command)) {
1228 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1231 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1232 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1236 if (onvifCamera.supportsPTZ()) {
1237 if (command instanceof IncreaseDecreaseType) {
1238 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1239 if (cameraConfig.getPtzContinuous()) {
1240 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1242 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1245 if (cameraConfig.getPtzContinuous()) {
1246 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1248 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1252 } else if (OnOffType.OFF.equals(command)) {
1253 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1256 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1257 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1261 if (onvifCamera.supportsPTZ()) {
1262 if (command instanceof IncreaseDecreaseType) {
1263 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1264 if (cameraConfig.getPtzContinuous()) {
1265 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1267 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1270 if (cameraConfig.getPtzContinuous()) {
1271 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1273 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1277 } else if (OnOffType.OFF.equals(command)) {
1278 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1281 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1282 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1287 // commands and refresh now get passed to brand handlers
1288 switch (thing.getThingTypeUID().getId()) {
1290 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1291 amcrestHandler.handleCommand(channelUID, command);
1292 if (lowPriorityRequests.isEmpty()) {
1293 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1297 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1298 dahuaHandler.handleCommand(channelUID, command);
1299 if (lowPriorityRequests.isEmpty()) {
1300 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1303 case DOORBIRD_THING:
1304 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1305 doorBirdHandler.handleCommand(channelUID, command);
1306 if (lowPriorityRequests.isEmpty()) {
1307 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1310 case HIKVISION_THING:
1311 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1312 hikvisionHandler.handleCommand(channelUID, command);
1315 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1316 cameraConfig.getPassword());
1317 foscamHandler.handleCommand(channelUID, command);
1318 if (lowPriorityRequests.isEmpty()) {
1319 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1323 InstarHandler instarHandler = new InstarHandler(getHandle());
1324 instarHandler.handleCommand(channelUID, command);
1325 if (lowPriorityRequests.isEmpty()) {
1326 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1330 ReolinkHandler reolinkHandler = new ReolinkHandler(getHandle());
1331 reolinkHandler.handleCommand(channelUID, command);
1334 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1335 defaultHandler.handleCommand(channelUID, command);
1336 if (lowPriorityRequests.isEmpty()) {
1337 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1343 public void setChannelState(String channelToUpdate, State valueOf) {
1344 updateState(channelToUpdate, valueOf);
1347 private void bringCameraOnline() {
1349 updateStatus(ThingStatus.ONLINE);
1350 groupTracker.listOfOnlineCameraHandlers.add(this);
1351 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1352 Future<?> localFuture = cameraConnectionJob;
1353 if (localFuture != null) {
1354 localFuture.cancel(false);
1355 cameraConnectionJob = null;
1357 if (!snapshotUri.isEmpty()) {
1358 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1359 snapshotPolling = true;
1360 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000,
1361 cameraConfig.getPollTime(), TimeUnit.MILLISECONDS);
1365 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1367 // auto restart mjpeg stream now camera is back online.
1368 CameraServlet localServlet = servlet;
1369 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1370 openCamerasStream();
1373 if (!rtspUri.isEmpty()) {
1374 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1376 if (updateImageChannel) {
1377 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1379 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1381 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1382 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1383 handle.cameraOnline(getThing().getUID().getId());
1388 void snapshotIsFfmpeg() {
1389 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1391 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1392 bringCameraOnline();
1393 if (!rtspUri.isEmpty()) {
1394 updateImageChannel = false;
1395 ffmpegSnapshotGeneration = true;
1396 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1397 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1399 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1403 void pollingCameraConnection() {
1405 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)
1406 || thing.getThingTypeUID().getId().equals(DOORBIRD_THING)) {
1407 if (rtspUri.isEmpty()) {
1408 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1410 if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1413 ffmpegSnapshotGeneration = false;
1418 if (cameraConfig.getOnvifPort() > 0 && !onvifCamera.isConnected()) {
1419 if (onvifCamera.isConnectError()) {
1420 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Camera is not reachable");
1421 } else if (onvifCamera.isRefusedError()) {
1422 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1423 "Camera refused connection on ONVIF ports.");
1425 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP: {}:{}", cameraConfig.getIp(),
1426 cameraConfig.getOnvifPort());
1427 onvifCamera.connect(supportsOnvifEvents());
1430 if ("ffmpeg".equals(snapshotUri)) {
1432 } else if (!snapshotUri.isEmpty()) {
1433 ffmpegSnapshotGeneration = false;
1435 } else if (!rtspUri.isEmpty()) {
1438 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1439 "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.");
1443 public void cameraConfigError(String reason) {
1444 // won't try to reconnect again due to a config error being the cause.
1445 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1449 public void cameraCommunicationError(String reason) {
1450 // will try to reconnect again as camera may be rebooting.
1451 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1452 if (isOnline) { // if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1453 resetAndRetryConnecting();
1457 private boolean streamIsStopped(String url) {
1458 ChannelTracking channelTracking = channelTrackingMap.get(url);
1459 if (channelTracking != null) {
1460 if (channelTracking.getChannel().isActive()) {
1461 return false; // stream is running.
1464 return true; // Stream stopped or never started.
1467 void snapshotRunnable() {
1468 // Snapshot should be first to keep consistent time between shots
1470 if (snapCount > 0) {
1471 if (--snapCount == 0) {
1472 setupFfmpegFormat(FFmpegFormat.GIF);
1477 private void takeSnapshot() {
1478 sendHttpGET(snapshotUri);
1481 private void updateSnapshot() {
1482 lastSnapshotRequest = Instant.now();
1483 mainEventLoopGroup.schedule(this::takeSnapshot, 0, TimeUnit.MILLISECONDS);
1486 public byte[] getSnapshot() {
1488 // Single gray pixel JPG to keep streams open when the camera goes offline so they dont stop.
1489 return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
1490 0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43,
1491 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
1492 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09,
1493 0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d,
1494 0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10,
1495 0x10, (byte) 0xff, (byte) 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
1496 (byte) 0xff, (byte) 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, (byte) 0xff, (byte) 0xda, 0x00, 0x08,
1497 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, (byte) 0xd2, (byte) 0xcf, 0x20, (byte) 0xff, (byte) 0xd9 };
1499 // Most cameras will return a 503 busy error if snapshot is faster than 1 second
1500 long lastUpdatedMs = Duration.between(lastSnapshotRequest, Instant.now()).toMillis();
1501 if (!snapshotPolling && !ffmpegSnapshotGeneration && lastUpdatedMs >= cameraConfig.getPollTime()) {
1504 lockCurrentSnapshot.lock();
1506 return currentSnapshot;
1508 lockCurrentSnapshot.unlock();
1512 public void stopSnapshotPolling() {
1513 Future<?> localFuture;
1514 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1515 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1516 snapshotPolling = false;
1517 localFuture = snapshotJob;
1518 if (localFuture != null) {
1519 localFuture.cancel(true);
1521 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1522 snapshotPolling = false;
1523 localFuture = snapshotJob;
1524 if (localFuture != null) {
1525 localFuture.cancel(true);
1530 public void startSnapshotPolling() {
1531 if (snapshotPolling || ffmpegSnapshotGeneration) {
1532 return; // Already polling or creating with FFmpeg from RTSP
1534 if (streamingSnapshotMjpeg || streamingAutoFps || cameraConfig.getUpdateImageWhen().contains("4")) {
1535 snapshotPolling = true;
1536 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 0, cameraConfig.getPollTime(),
1537 TimeUnit.MILLISECONDS);
1542 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep alarm
1543 * streams open and more.
1546 void pollCameraRunnable() {
1547 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1548 if (!lowPriorityRequests.isEmpty()) {
1549 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1550 lowPriorityCounter = 0;
1552 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1554 // what needs to be done every poll//
1555 switch (thing.getThingTypeUID().getId()) {
1557 if (!snapshotPolling) {
1558 checkCameraConnection();
1562 if (!snapshotPolling) {
1563 checkCameraConnection();
1567 if (!snapshotPolling) {
1568 checkCameraConnection();
1570 noMotionDetected(CHANNEL_MOTION_ALARM);
1571 noMotionDetected(CHANNEL_PIR_ALARM);
1572 noMotionDetected(CHANNEL_HUMAN_ALARM);
1573 noMotionDetected(CHANNEL_CAR_ALARM);
1574 noMotionDetected(CHANNEL_ANIMAL_ALARM);
1577 case HIKVISION_THING:
1578 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1579 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1580 cameraConfig.getIp());
1581 sendHttpGET("/ISAPI/Event/notification/alertStream");
1585 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1586 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1589 if (cameraConfig.getNvrChannel() > 0) {
1590 sendHttpGET("/api.cgi?cmd=GetAiState&channel=" + cameraConfig.getNvrChannel() + "&user="
1591 + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword());
1592 sendHttpGET("/api.cgi?cmd=GetMdState&channel=" + cameraConfig.getNvrChannel() + "&user="
1593 + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword());
1595 if (!snapshotPolling) {
1596 checkCameraConnection();
1598 if (!onvifCamera.isConnected()) {
1599 onvifCamera.connect(true);
1604 if (!snapshotPolling) {
1605 checkCameraConnection();
1607 // Check for alarms, channel for NVRs appears not to work at filtering.
1608 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1609 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1610 cameraConfig.getIp());
1611 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1614 case DOORBIRD_THING:
1615 if (!snapshotPolling) {
1616 checkCameraConnection();
1618 // Check for alarms, channel for NVRs appears not to work at filtering.
1619 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1620 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1621 cameraConfig.getIp());
1622 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1626 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1627 + cameraConfig.getPassword());
1630 Ffmpeg localFfmpeg = ffmpegHLS;
1631 if (localFfmpeg != null) {
1632 localFfmpeg.checkKeepAlive();
1634 if (ffmpegMotionAlarmEnabled || ffmpegAudioAlarmEnabled) {
1635 localFfmpeg = ffmpegRtspHelper;
1636 if (localFfmpeg == null || !localFfmpeg.isAlive()) {
1637 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1640 // check if the thread has frozen due to camera doing a soft reboot
1641 localFfmpeg = ffmpegMjpeg;
1642 if (localFfmpeg != null && !localFfmpeg.isAlive()) {
1643 logger.debug("MJPEG was not being produced by FFmpeg when it should have been, restarting FFmpeg.");
1644 setupFfmpegFormat(FFmpegFormat.MJPEG);
1646 if (openChannels.size() > 10) {
1647 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1653 public void initialize() {
1654 cameraConfig = getConfigAs(CameraConfig.class);
1655 threadPool = Executors.newScheduledThreadPool(2);
1656 mainEventLoopGroup = new NioEventLoopGroup(3);
1657 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1658 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1659 rtspUri = cameraConfig.getFfmpegInput();
1660 if (cameraConfig.getFfmpegOutput().isEmpty()) {
1662 .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1664 // Known cameras will connect quicker if we skip ONVIF questions.
1665 switch (thing.getThingTypeUID().getId()) {
1668 if (mjpegUri.isEmpty()) {
1669 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1671 if (snapshotUri.isEmpty()) {
1672 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1675 case DOORBIRD_THING:
1676 if (mjpegUri.isEmpty()) {
1677 mjpegUri = "/bha-api/video.cgi";
1679 if (snapshotUri.isEmpty()) {
1680 snapshotUri = "/bha-api/image.cgi";
1684 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1685 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1686 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1687 if (mjpegUri.isEmpty()) {
1688 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1689 + cameraConfig.getPassword();
1691 if (snapshotUri.isEmpty()) {
1692 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1693 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1696 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1697 if (mjpegUri.isEmpty()) {
1698 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1700 if (snapshotUri.isEmpty()) {
1701 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1703 if (lowPriorityRequests.isEmpty()) {
1704 lowPriorityRequests.add("/ISAPI/System/IO/inputs/" + cameraConfig.getNvrChannel() + "/status");
1708 if (snapshotUri.isEmpty()) {
1709 snapshotUri = "/tmpfs/snap.jpg";
1711 if (mjpegUri.isEmpty()) {
1712 mjpegUri = "/mjpegstream.cgi?-chn=12";
1714 // Newer Instar cameras use this to setup the Alarm Server, plus it is used to work out which API is
1715 // implemented based on the response to these two requests.
1717 "/param.cgi?cmd=setasaction&-server=1&enable=1&-interval=1&cmd=setasattr&-as_index=1&-as_server="
1718 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1719 + getThing().getUID().getId()
1720 + "/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");
1721 // Older Instar cameras use this to setup the Alarm Server
1723 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
1724 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1725 + getThing().getUID().getId()
1726 + "/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");
1729 if (cameraConfig.useToken) {
1730 authenticationJob = threadPool.scheduleWithFixedDelay(this::getReolinkToken, 0, 45,
1733 reolinkAuth = "&user=" + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword();
1735 if (snapshotUri.isEmpty()) {
1736 if (cameraConfig.getNvrChannel() < 1) {
1737 snapshotUri = "/cgi-bin/api.cgi?cmd=Snap&channel=0&rs=openHAB" + reolinkAuth;
1739 snapshotUri = "/cgi-bin/api.cgi?cmd=Snap&channel=" + (cameraConfig.getNvrChannel() - 1)
1740 + "&rs=openHAB" + reolinkAuth;
1743 if (rtspUri.isEmpty()) {
1744 if (cameraConfig.getNvrChannel() < 1) {
1745 rtspUri = "rtsp://" + cameraConfig.getIp() + ":554/h264Preview_01_main";
1747 rtspUri = "rtsp://" + cameraConfig.getIp() + ":554/h264Preview_0" + cameraConfig.getNvrChannel()
1753 // for poll times 9 seconds and above don't display a warning about the Image channel.
1754 if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1756 "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.");
1758 // ONVIF and Instar event handling need the server started before connecting.
1759 startStreamServer();
1763 private void tryConnecting() {
1765 int normalDelay = 12; // doesn't make sense to have faster retry than CONNECT_TIMEOUT, which is 10 seconds, if
1767 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)
1768 && !thing.getThingTypeUID().getId().equals(DOORBIRD_THING) && cameraConfig.getOnvifPort() > 0) {
1769 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1770 cameraConfig.getUser(), cameraConfig.getPassword());
1771 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1772 // Only use ONVIF events if it is not an API camera.
1773 onvifCamera.connect(supportsOnvifEvents());
1775 if (supportsOnvifEvents()) {
1776 // it takes some time to try to retrieve the ONVIF snapshot and stream URLs and update internal members
1777 // on first connect; if connection lost, doesn't make sense to poll to often
1782 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, firstDelay, normalDelay,
1786 private boolean supportsOnvifEvents() {
1787 switch (thing.getThingTypeUID().getId()) {
1791 if (cameraConfig.getNvrChannel() < 1) {
1798 private void keepMjpegRunning() {
1799 CameraServlet localServlet = servlet;
1800 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1801 if (!mjpegUri.isEmpty() && !"ffmpeg".equals(mjpegUri)) {
1802 localServlet.openStreams.queueFrame(("--" + localServlet.openStreams.boundary + "\r\n\r\n").getBytes());
1804 localServlet.openStreams.queueFrame(getSnapshot());
1808 // What the camera needs to re-connect if the initialize() is not called.
1809 private void resetAndRetryConnecting() {
1814 private void offline() {
1816 snapshotPolling = false;
1817 Future<?> localFuture = pollCameraJob;
1818 if (localFuture != null) {
1819 localFuture.cancel(true);
1820 pollCameraJob = null;
1822 localFuture = authenticationJob;
1823 if (localFuture != null) {
1824 localFuture.cancel(true);
1825 authenticationJob = null;
1827 localFuture = snapshotJob;
1828 if (localFuture != null) {
1829 localFuture.cancel(true);
1832 localFuture = cameraConnectionJob;
1833 if (localFuture != null) {
1834 localFuture.cancel(true);
1835 cameraConnectionJob = null;
1837 Ffmpeg localFfmpeg = ffmpegHLS;
1838 if (localFfmpeg != null) {
1839 localFfmpeg.stopConverting();
1842 localFfmpeg = ffmpegRecord;
1843 if (localFfmpeg != null) {
1844 localFfmpeg.stopConverting();
1845 ffmpegRecord = null;
1847 localFfmpeg = ffmpegGIF;
1848 if (localFfmpeg != null) {
1849 localFfmpeg.stopConverting();
1852 localFfmpeg = ffmpegRtspHelper;
1853 if (localFfmpeg != null) {
1854 localFfmpeg.stopConverting();
1855 ffmpegRtspHelper = null;
1857 localFfmpeg = ffmpegMjpeg;
1858 if (localFfmpeg != null) {
1859 localFfmpeg.stopConverting();
1862 localFfmpeg = ffmpegSnapshot;
1863 if (localFfmpeg != null) {
1864 localFfmpeg.stopConverting();
1865 ffmpegSnapshot = null;
1867 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) { // generic cameras do not have ONVIF support
1868 onvifCamera.disconnect();
1870 openChannels.close();
1874 public void dispose() {
1876 CameraServlet localServlet = servlet;
1877 if (localServlet != null) {
1878 localServlet.dispose();
1881 threadPool.shutdown();
1882 // inform all group handlers that this camera has gone offline
1883 groupTracker.listOfOnlineCameraHandlers.remove(this);
1884 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1885 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1886 handle.cameraOffline(this);
1888 basicAuth = ""; // clear out stored Password hash
1889 useDigestAuth = false;
1890 mainEventLoopGroup.shutdownGracefully();
1891 mainBootstrap = null;
1892 channelTrackingMap.clear();
1895 public String getWhiteList() {
1896 return cameraConfig.getIpWhitelist();
1900 public Collection<Class<? extends ThingHandlerService>> getServices() {
1901 return Set.of(IpCameraActions.class);