2 * Copyright (c) 2010-2024 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.atomic.AtomicBoolean;
42 import java.util.concurrent.locks.ReentrantLock;
44 import org.eclipse.jdt.annotation.NonNullByDefault;
45 import org.eclipse.jdt.annotation.Nullable;
46 import org.openhab.binding.ipcamera.internal.AmcrestHandler;
47 import org.openhab.binding.ipcamera.internal.CameraConfig;
48 import org.openhab.binding.ipcamera.internal.ChannelTracking;
49 import org.openhab.binding.ipcamera.internal.DahuaHandler;
50 import org.openhab.binding.ipcamera.internal.DoorBirdHandler;
51 import org.openhab.binding.ipcamera.internal.Ffmpeg;
52 import org.openhab.binding.ipcamera.internal.FoscamHandler;
53 import org.openhab.binding.ipcamera.internal.GroupTracker;
54 import org.openhab.binding.ipcamera.internal.Helper;
55 import org.openhab.binding.ipcamera.internal.HikvisionHandler;
56 import org.openhab.binding.ipcamera.internal.HttpOnlyHandler;
57 import org.openhab.binding.ipcamera.internal.InstarHandler;
58 import org.openhab.binding.ipcamera.internal.IpCameraActions;
59 import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
60 import org.openhab.binding.ipcamera.internal.IpCameraDynamicStateDescriptionProvider;
61 import org.openhab.binding.ipcamera.internal.MyNettyAuthHandler;
62 import org.openhab.binding.ipcamera.internal.ReolinkHandler;
63 import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection;
64 import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection.RequestType;
65 import org.openhab.binding.ipcamera.internal.servlet.CameraServlet;
66 import org.openhab.core.OpenHAB;
67 import org.openhab.core.library.types.DecimalType;
68 import org.openhab.core.library.types.IncreaseDecreaseType;
69 import org.openhab.core.library.types.OnOffType;
70 import org.openhab.core.library.types.PercentType;
71 import org.openhab.core.library.types.RawType;
72 import org.openhab.core.library.types.StringType;
73 import org.openhab.core.thing.ChannelUID;
74 import org.openhab.core.thing.Thing;
75 import org.openhab.core.thing.ThingStatus;
76 import org.openhab.core.thing.ThingStatusDetail;
77 import org.openhab.core.thing.binding.BaseThingHandler;
78 import org.openhab.core.thing.binding.ThingHandlerService;
79 import org.openhab.core.thing.binding.builder.ThingBuilder;
80 import org.openhab.core.types.Command;
81 import org.openhab.core.types.RefreshType;
82 import org.openhab.core.types.State;
83 import org.osgi.framework.FrameworkUtil;
84 import org.osgi.service.http.HttpService;
85 import org.slf4j.Logger;
86 import org.slf4j.LoggerFactory;
88 import io.netty.bootstrap.Bootstrap;
89 import io.netty.buffer.ByteBuf;
90 import io.netty.buffer.Unpooled;
91 import io.netty.channel.Channel;
92 import io.netty.channel.ChannelDuplexHandler;
93 import io.netty.channel.ChannelFuture;
94 import io.netty.channel.ChannelFutureListener;
95 import io.netty.channel.ChannelHandlerContext;
96 import io.netty.channel.ChannelInitializer;
97 import io.netty.channel.ChannelOption;
98 import io.netty.channel.EventLoopGroup;
99 import io.netty.channel.group.ChannelGroup;
100 import io.netty.channel.group.DefaultChannelGroup;
101 import io.netty.channel.nio.NioEventLoopGroup;
102 import io.netty.channel.socket.SocketChannel;
103 import io.netty.channel.socket.nio.NioSocketChannel;
104 import io.netty.handler.codec.base64.Base64;
105 import io.netty.handler.codec.http.DefaultFullHttpRequest;
106 import io.netty.handler.codec.http.FullHttpRequest;
107 import io.netty.handler.codec.http.HttpClientCodec;
108 import io.netty.handler.codec.http.HttpContent;
109 import io.netty.handler.codec.http.HttpHeaderValues;
110 import io.netty.handler.codec.http.HttpMessage;
111 import io.netty.handler.codec.http.HttpMethod;
112 import io.netty.handler.codec.http.HttpResponse;
113 import io.netty.handler.codec.http.HttpVersion;
114 import io.netty.handler.codec.http.LastHttpContent;
115 import io.netty.handler.timeout.IdleState;
116 import io.netty.handler.timeout.IdleStateEvent;
117 import io.netty.handler.timeout.IdleStateHandler;
118 import io.netty.util.CharsetUtil;
119 import io.netty.util.ReferenceCountUtil;
120 import io.netty.util.concurrent.GlobalEventExecutor;
123 * The {@link IpCameraHandler} is responsible for handling commands, which are
124 * sent to one of the channels.
126 * @author Matthew Skinner - Initial contribution
129 public class IpCameraHandler extends BaseThingHandler {
130 public final Logger logger = LoggerFactory.getLogger(getClass());
131 public final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
132 private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2);
133 private GroupTracker groupTracker;
134 public CameraConfig cameraConfig = new CameraConfig();
136 // ChannelGroup is thread safe
137 public final ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
138 private final HttpService httpService;
139 private @Nullable CameraServlet servlet;
140 public String mjpegContentType = "";
141 public @Nullable Ffmpeg ffmpegHLS = null;
142 public @Nullable Ffmpeg ffmpegRecord = null;
143 public @Nullable Ffmpeg ffmpegGIF = null;
144 public @Nullable Ffmpeg ffmpegRtspHelper = null;
145 public @Nullable Ffmpeg ffmpegMjpeg = null;
146 public @Nullable Ffmpeg ffmpegSnapshot = null;
147 public boolean streamingAutoFps = false;
148 public boolean motionDetected = false;
149 public Instant lastSnapshotRequest = Instant.now();
150 public Instant currentSnapshotTime = Instant.now();
151 private @Nullable ScheduledFuture<?> cameraConnectionJob = null;
152 private @Nullable ScheduledFuture<?> pollCameraJob = null;
153 private @Nullable ScheduledFuture<?> snapshotJob = null;
154 private @Nullable ScheduledFuture<?> authenticationJob = null;
155 private @Nullable Bootstrap mainBootstrap;
156 private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup(1);
157 private FullHttpRequest putRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT, "");
158 private FullHttpRequest postRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "");
159 private String gifFilename = "ipcamera";
160 private String gifHistory = "";
161 private String mp4History = "";
162 public int gifHistoryLength;
163 public int mp4HistoryLength;
164 private String mp4Filename = "ipcamera";
165 private int mp4RecordTime;
166 private int gifRecordTime = 5;
167 private LinkedList<byte[]> fifoSnapshotBuffer = new LinkedList<>();
168 private int snapCount;
169 private boolean updateImageChannel = false;
170 private byte lowPriorityCounter = 0;
171 public String hostIp;
172 public Map<String, ChannelTracking> channelTrackingMap = new ConcurrentHashMap<>();
173 public List<String> lowPriorityRequests = new ArrayList<>(0);
175 // basicAuth MUST remain private as it holds the cameraConfig.getPassword()
176 private String basicAuth = "";
177 public String reolinkAuth = "&token=null";
178 public int reolinkScheduleVersion = 0;
179 public boolean useBasicAuth = false;
180 public boolean useDigestAuth = false;
181 public boolean newInstarApi = false;
182 public String snapshotUri = "";
183 public String mjpegUri = "";
184 private byte[] currentSnapshot = new byte[] { (byte) 0x00 };
185 public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
186 public String rtspUri = "";
187 public boolean audioAlarmUpdateSnapshot = false;
188 private boolean motionAlarmUpdateSnapshot = false;
189 private AtomicBoolean isOnline = new AtomicBoolean(); // Used so only 1 error is logged when a network issue occurs.
190 private boolean firstAudioAlarm = false;
191 private boolean firstMotionAlarm = false;
192 public BigDecimal motionThreshold = BigDecimal.ZERO;
193 public int audioThreshold = 35;
194 public boolean streamingSnapshotMjpeg = false;
195 public boolean ffmpegMotionAlarmEnabled = false;
196 public boolean ffmpegAudioAlarmEnabled = false;
197 public boolean ffmpegSnapshotGeneration = false;
198 public boolean snapshotPolling = false;
199 public OnvifConnection onvifCamera = new OnvifConnection(this, "", "", "");
201 // These methods handle the response from all camera brands, nothing specific to 1 brand.
202 private class CommonCameraHandler extends ChannelDuplexHandler {
203 private int bytesToRecieve = 0;
204 private int bytesAlreadyRecieved = 0;
205 private byte[] incomingJpeg = new byte[0];
206 private String incomingMessage = "";
207 private String contentType = "empty";
208 private String boundary = "";
209 private Object reply = new Object();
210 private String requestUrl = "";
211 private boolean isChunked = false;
213 public void setURL(String url) {
218 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
219 if (msg == null || ctx == null) {
223 if (msg instanceof HttpResponse response) {
224 if (response.status().code() == 200) {
225 if (!response.headers().isEmpty()) {
226 for (String name : response.headers().names()) {
227 // Some cameras use first letter uppercase and others dont.
228 switch (name.toLowerCase()) { // Possible localization issues doing this
230 contentType = response.headers().getAsString(name);
232 case "content-length":
233 bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
235 case "transfer-encoding":
236 if (response.headers().getAsString(name).contains("chunked")) {
242 if (contentType.contains("multipart")) {
243 boundary = Helper.searchString(contentType, "boundary=");
244 if (mjpegUri.endsWith(requestUrl)) {
245 if (msg instanceof HttpMessage) {
246 // very start of stream only
247 mjpegContentType = contentType;
248 CameraServlet localServlet = servlet;
249 if (localServlet != null) {
250 logger.debug("Setting Content-Type to: {}", contentType);
251 localServlet.openStreams.updateContentType(contentType, boundary);
255 } else if (contentType.contains("image/jp")) {
256 if (bytesToRecieve == 0) {
257 bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
258 logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
260 incomingJpeg = new byte[bytesToRecieve];
264 // Non 200 OK replies are logged and handled in pipeline by MyNettyAuthHandler.java
268 if (msg instanceof HttpContent content) {
269 if (mjpegUri.equals(requestUrl) && !(content instanceof LastHttpContent)) {
270 // multiple MJPEG stream packets come back as this.
271 byte[] chunkedFrame = new byte[content.content().readableBytes()];
272 content.content().getBytes(content.content().readerIndex(), chunkedFrame);
273 CameraServlet localServlet = servlet;
274 if (localServlet != null) {
275 localServlet.openStreams.queueFrame(chunkedFrame);
278 // Found some cameras use Content-Type: image/jpg instead of image/jpeg
279 if (contentType.contains("image/jp")) {
280 for (int i = 0; i < content.content().capacity(); i++) {
281 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
283 if (content instanceof LastHttpContent) {
284 processSnapshot(incomingJpeg);
287 } else { // incomingMessage that is not an IMAGE
288 if (incomingMessage.isEmpty()) {
289 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
291 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
293 bytesAlreadyRecieved = incomingMessage.length();
294 if (content instanceof LastHttpContent) {
295 // If it is not an image send it on to the next handler//
296 if (bytesAlreadyRecieved != 0) {
297 reply = incomingMessage;
298 super.channelRead(ctx, reply);
301 // Alarm Streams never have a LastHttpContent as they always stay open//
302 else if (contentType.contains("multipart")) {
303 int beginIndex, endIndex;
304 if (bytesToRecieve == 0) {
305 beginIndex = incomingMessage.indexOf("Content-Length:");
306 if (beginIndex != -1) {
307 endIndex = incomingMessage.indexOf("\r\n", beginIndex);
308 if (endIndex != -1) {
309 bytesToRecieve = Integer.parseInt(
310 incomingMessage.substring(beginIndex + 15, endIndex).strip());
314 // --boundary and headers are not included in the Content-Length value
315 if (bytesAlreadyRecieved > bytesToRecieve) {
316 // Check if message has a second --boundary
317 endIndex = incomingMessage.indexOf("--" + boundary, bytesToRecieve);
318 if (endIndex == -1) {
319 reply = incomingMessage;
320 incomingMessage = "";
322 bytesAlreadyRecieved = 0;
324 reply = incomingMessage.substring(0, endIndex);
325 incomingMessage = incomingMessage.substring(endIndex, incomingMessage.length());
326 bytesToRecieve = 0;// Triggers search next time for Content-Length:
327 bytesAlreadyRecieved = incomingMessage.length() - endIndex;
329 super.channelRead(ctx, reply);
332 // Foscam needs this as will other cameras with chunks//
333 if (isChunked && bytesAlreadyRecieved != 0) {
334 reply = incomingMessage;
338 } else { // msg is not HttpContent
339 // Foscam cameras need this
340 if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
341 reply = incomingMessage;
342 logger.trace("Packet back from camera is {}", incomingMessage);
343 super.channelRead(ctx, reply);
347 ReferenceCountUtil.release(msg);
352 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
353 if (cause == null || ctx == null) {
356 if (cause instanceof ArrayIndexOutOfBoundsException) {
357 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
360 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
367 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
371 if (evt instanceof IdleStateEvent e) {
372 // If camera does not use the channel for X amount of time it will close.
373 if (e.state() == IdleState.READER_IDLE) {
374 String urlToKeepOpen = "";
375 switch (thing.getThingTypeUID().getId()) {
377 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
380 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
383 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
384 if (channelTracking != null) {
385 if (channelTracking.getChannel().equals(ctx.channel())) {
386 return; // don't auto close this as it is for the alarms.
389 logger.debug("Closing an idle channel for camera: {}", cameraConfig.getIp());
396 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
397 IpCameraDynamicStateDescriptionProvider stateDescriptionProvider, HttpService httpService) {
399 this.stateDescriptionProvider = stateDescriptionProvider;
400 if (ipAddress != null) {
403 hostIp = Helper.getLocalIpAddress();
405 this.groupTracker = groupTracker;
406 this.httpService = httpService;
409 private IpCameraHandler getHandle() {
413 // false clears the stored user/pass hash, true creates the hash
414 public boolean setBasicAuth(boolean useBasic) {
416 logger.debug("Clearing out the stored BASIC auth now.");
419 } else if (!basicAuth.isEmpty()) {
420 // If the binding sends multiple requests before basicAuth was set, this may trigger falsely.
421 logger.warn("Camera is reporting your username and/or password is wrong.");
424 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
425 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
426 ByteBuf byteBuf = null;
428 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
429 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
431 if (byteBuf != null) {
437 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
442 public String getCorrectUrlFormat(String longUrl) {
443 String temp = longUrl;
446 if (longUrl.isEmpty() || "ffmpeg".equals(longUrl)) {
451 url = new URL(longUrl);
452 int port = url.getPort();
454 if (url.getQuery() == null) {
455 temp = url.getPath();
457 temp = url.getPath() + "?" + url.getQuery();
460 if (url.getQuery() == null) {
461 temp = ":" + url.getPort() + url.getPath();
463 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
466 } catch (MalformedURLException e) {
467 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
472 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
473 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
474 sendHttpRequest("PUT", httpRequestURL, null);
477 public void sendHttpPOST(String httpPostURL, String content) {
478 FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, httpPostURL);
479 request.headers().set("Host", cameraConfig.getIp());
480 request.headers().add("Content-Type", "application/json");
481 request.headers().add("User-Agent",
482 "openHAB/" + FrameworkUtil.getBundle(this.getClass()).getVersion().toString());
483 request.headers().add("Accept", "*/*");
484 ByteBuf bbuf = Unpooled.copiedBuffer(content, StandardCharsets.UTF_8);
485 request.headers().set("Content-Length", bbuf.readableBytes());
486 request.content().clear().writeBytes(bbuf);
487 postRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
488 sendHttpRequest("POST", httpPostURL, null);
491 public void sendHttpPOST(String httpRequestURL) {
492 sendHttpRequest("POST", httpRequestURL, null);
495 public void sendHttpGET(String httpRequestURL) {
496 sendHttpRequest("GET", httpRequestURL, null);
499 public int getPortFromShortenedUrl(String httpRequestURL) {
500 if (httpRequestURL.startsWith(":")) {
501 int end = httpRequestURL.indexOf("/");
502 return Integer.parseInt(httpRequestURL.substring(1, end));
504 return cameraConfig.getPort();
507 public String getTinyUrl(String httpRequestURL) {
508 if (httpRequestURL.startsWith(":")) {
509 int beginIndex = httpRequestURL.indexOf("/");
510 return httpRequestURL.substring(beginIndex);
512 return httpRequestURL;
515 private void checkCameraConnection() {
516 if (snapshotPolling) { // Currently polling a real URL for snapshots, so camera must be online.
518 } else if (ffmpegSnapshotGeneration) {
519 Ffmpeg localSnapshot = ffmpegSnapshot;
520 if (localSnapshot != null && !localSnapshot.isAlive()) {
521 cameraCommunicationError("FFmpeg Snapshots Stopped: Check that your camera can be reached.");
523 return; // RTSP stream is creating snapshots, so camera is online.
526 if (supportsOnvifEvents() && onvifCamera.isConnected() && onvifCamera.getEventsSupported()) {
527 return;// ONVIF cameras that are getting event messages must be online
530 // Open a HTTP connection without sending any requests as we do not need a snapshot.
531 Bootstrap localBootstrap = mainBootstrap;
532 if (localBootstrap != null) {
533 ChannelFuture chFuture = localBootstrap
534 .connect(new InetSocketAddress(cameraConfig.getIp(), cameraConfig.getPort()));
535 if (chFuture.awaitUninterruptibly(500)) {
536 chFuture.channel().close();
540 cameraCommunicationError("Connection Timeout: Check your IP:" + cameraConfig.getIp() + " and PORT:"
541 + cameraConfig.getPort() + " are correct and the camera can be reached.");
544 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
545 // The authHandler will generate a digest string and re-send using this same function when needed.
546 @SuppressWarnings("null")
547 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
548 int port = getPortFromShortenedUrl(httpRequestURLFull);
549 String httpRequestURL = getTinyUrl(httpRequestURLFull);
550 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port, httpRequestURL);
551 if (mainBootstrap == null) {
552 mainBootstrap = new Bootstrap();
553 mainBootstrap.group(mainEventLoopGroup);
554 mainBootstrap.channel(NioSocketChannel.class);
555 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
556 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
557 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
558 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
559 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
560 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
563 public void initChannel(SocketChannel socketChannel) throws Exception {
564 // HIK Alarm stream needs > 9sec idle to stop stream closing
565 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
566 socketChannel.pipeline().addLast(new HttpClientCodec());
567 socketChannel.pipeline().addLast(AUTH_HANDLER,
568 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
569 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
571 switch (thing.getThingTypeUID().getId()) {
573 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
576 socketChannel.pipeline()
577 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
580 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
583 socketChannel.pipeline().addLast(
584 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
586 case HIKVISION_THING:
587 socketChannel.pipeline().addLast(HIKVISION_HANDLER,
588 new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
591 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
594 socketChannel.pipeline().addLast(REOLINK_HANDLER, new ReolinkHandler(getHandle()));
597 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
604 FullHttpRequest request;
605 if ("GET".equals(httpMethod) || (useDigestAuth && digestString == null)) {
606 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
607 request.headers().set("Host", cameraConfig.getIp() + ":" + port);
608 request.headers().set("Connection", HttpHeaderValues.CLOSE);
609 } else if ("PUT".equals(httpMethod)) {
610 request = putRequestWithBody;
612 request = postRequestWithBody;
615 if (!basicAuth.isEmpty()) {
617 logger.warn("Camera at IP: {} had both Basic and Digest set to be used", cameraConfig.getIp());
620 request.headers().set("Authorization", "Basic " + basicAuth);
625 if (digestString != null) {
626 request.headers().set("Authorization", "Digest " + digestString);
630 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
631 .addListener(new ChannelFutureListener() {
634 public void operationComplete(@Nullable ChannelFuture future) {
635 if (future == null) {
638 if (future.isDone() && future.isSuccess()) {
639 Channel ch = future.channel();
640 openChannels.add(ch);
641 if (cameraConnectionJob != null && !isOnline.get()) {
644 openChannel(ch, httpRequestURL);
645 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
646 commonHandler.setURL(httpRequestURLFull);
647 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
648 authHandler.setURL(httpMethod, httpRequestURL);
650 switch (thing.getThingTypeUID().getId()) {
652 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
653 amcrestHandler.setURL(httpRequestURL);
655 case HIKVISION_THING:
656 HikvisionHandler hikvisionHandler = (HikvisionHandler) ch.pipeline()
657 .get(HIKVISION_HANDLER);
658 hikvisionHandler.setURL(httpRequestURL);
661 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
662 instarHandler.setURL(httpRequestURL);
665 ReolinkHandler reolinkHandler = (ReolinkHandler) ch.pipeline().get(REOLINK_HANDLER);
666 reolinkHandler.setURL(httpRequestURL);
669 ch.writeAndFlush(request);
670 } else { // an error occurred
671 cameraCommunicationError(
672 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
678 public void processSnapshot(byte[] incommingSnapshot) {
679 lockCurrentSnapshot.lock();
681 currentSnapshot = incommingSnapshot;
682 if (cameraConfig.getGifPreroll() > 0) {
683 fifoSnapshotBuffer.add(incommingSnapshot);
684 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
685 fifoSnapshotBuffer.removeFirst();
689 lockCurrentSnapshot.unlock();
690 currentSnapshotTime = Instant.now();
693 if (updateImageChannel) {
694 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
695 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
696 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
697 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
698 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
699 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
700 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
704 public void startStreamServer() {
705 servlet = new CameraServlet(this, httpService);
706 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
707 + getThing().getUID().getId() + "/ipcamera.m3u8"));
708 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
709 + getThing().getUID().getId() + "/ipcamera.jpg"));
710 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
711 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
714 public void openCamerasStream() {
715 if (mjpegUri.isEmpty() || "ffmpeg".equals(mjpegUri)) {
716 setupFfmpegFormat(FFmpegFormat.MJPEG);
719 closeChannel(getTinyUrl(mjpegUri));
720 // Dahua cameras crash if you refresh (close and open) the stream without this delay.
721 mainEventLoopGroup.schedule(this::openMjpegStream, 300, TimeUnit.MILLISECONDS);
724 private void openMjpegStream() {
725 sendHttpGET(mjpegUri);
728 private void openChannel(Channel channel, String httpRequestURL) {
729 ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
730 if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
731 tracker.setChannel(channel);
734 channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
737 public void closeChannel(String url) {
738 ChannelTracking channelTracking = channelTrackingMap.get(url);
739 if (channelTracking != null) {
740 if (channelTracking.getChannel().isOpen()) {
741 channelTracking.getChannel().close();
748 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
749 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
752 private void cleanChannels() {
753 for (Channel channel : openChannels) {
754 boolean oldChannel = true;
755 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
756 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
757 channelTrackingMap.remove(channelTracking.getRequestUrl());
759 if (channelTracking.getChannel().equals(channel)) {
760 logger.debug("Open channel to camera is used for URL: {}", channelTracking.getRequestUrl());
770 public void storeHttpReply(String url, String content) {
771 ChannelTracking channelTracking = channelTrackingMap.get(url);
772 if (channelTracking != null) {
773 channelTracking.setReply(content);
777 private void storeSnapshots() {
779 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
780 lockCurrentSnapshot.lock();
782 for (byte[] foo : fifoSnapshotBuffer) {
783 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
786 OutputStream fos = new FileOutputStream(file);
789 } catch (FileNotFoundException e) {
790 logger.warn("FileNotFoundException {}", e.getMessage());
791 } catch (IOException e) {
792 logger.warn("IOException {}", e.getMessage());
796 lockCurrentSnapshot.unlock();
800 public void setupFfmpegFormat(FFmpegFormat format) {
801 String inputOptions = cameraConfig.getFfmpegInputOptions();
802 if (cameraConfig.getFfmpegOutput().isEmpty()) {
803 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
806 if (rtspUri.isEmpty()) {
807 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
810 if (cameraConfig.getFfmpegLocation().isEmpty()) {
811 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
814 if (rtspUri.toLowerCase().contains("rtsp")) {
815 if (inputOptions.isEmpty()) {
816 inputOptions = "-rtsp_transport tcp";
820 // Make sure the folder exists, if not create it.
821 new File(cameraConfig.getFfmpegOutput()).mkdirs();
824 if (ffmpegHLS == null) {
825 if (!inputOptions.isEmpty()) {
826 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
827 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
828 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
829 cameraConfig.getUser(), cameraConfig.getPassword());
831 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
832 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
833 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
834 cameraConfig.getPassword());
837 Ffmpeg localHLS = ffmpegHLS;
838 if (localHLS != null) {
839 localHLS.startConverting();
843 if (cameraConfig.getGifPreroll() > 0) {
844 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
845 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
846 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
847 + cameraConfig.getGifOutOptions(),
848 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
849 cameraConfig.getPassword());
851 if (!inputOptions.isEmpty()) {
852 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
854 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
856 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
857 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
858 cameraConfig.getUser(), cameraConfig.getPassword());
860 if (cameraConfig.getGifPreroll() > 0) {
863 Ffmpeg localGIF = ffmpegGIF;
864 if (localGIF != null) {
865 localGIF.startConverting();
866 if (gifHistory.isEmpty()) {
867 gifHistory = gifFilename;
868 } else if (!"ipcamera".equals(gifFilename)) {
869 gifHistory = gifFilename + "," + gifHistory;
870 if (gifHistoryLength > 49) {
871 int endIndex = gifHistory.lastIndexOf(",");
872 gifHistory = gifHistory.substring(0, endIndex);
875 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
879 if (!inputOptions.isEmpty()) {
880 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
882 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
884 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
885 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
886 cameraConfig.getUser(), cameraConfig.getPassword());
887 ffmpegRecord.startConverting();
888 if (mp4History.isEmpty()) {
889 mp4History = mp4Filename;
890 } else if (!"ipcamera".equals(mp4Filename)) {
891 mp4History = mp4Filename + "," + mp4History;
892 if (mp4HistoryLength > 49) {
893 int endIndex = mp4History.lastIndexOf(",");
894 mp4History = mp4History.substring(0, endIndex);
897 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
900 Ffmpeg localAlarms = ffmpegRtspHelper;
901 if (localAlarms != null) {
902 localAlarms.stopConverting();
903 if (!ffmpegAudioAlarmEnabled && !ffmpegMotionAlarmEnabled) {
907 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
908 String filterOptions = "";
909 if (!ffmpegAudioAlarmEnabled) {
910 filterOptions = "-an";
912 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
914 if (!ffmpegMotionAlarmEnabled && !ffmpegSnapshotGeneration) {
915 filterOptions = filterOptions.concat(" -vn");
916 } else if (ffmpegMotionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
917 String usersMotionOptions = cameraConfig.getMotionOptions();
918 if (usersMotionOptions.startsWith("-")) {
919 // Need to put the users custom options first in the chain before the motion is detected
920 filterOptions += " " + usersMotionOptions + ",select='gte(scene,"
921 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
923 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
924 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
926 } else if (ffmpegMotionAlarmEnabled) {
927 filterOptions = filterOptions.concat(" -vf select='gte(scene,"
928 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print");
930 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
931 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
932 ffmpegRtspHelper.startConverting();
935 if (ffmpegMjpeg == null) {
936 if (inputOptions.isEmpty()) {
937 inputOptions = "-hide_banner";
939 inputOptions += " -hide_banner";
941 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
942 cameraConfig.getMjpegOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
943 + getThing().getUID().getId() + "/ipcamera.jpg",
944 cameraConfig.getUser(), cameraConfig.getPassword());
946 Ffmpeg localMjpeg = ffmpegMjpeg;
947 if (localMjpeg != null) {
948 localMjpeg.startConverting();
952 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
953 if (ffmpegSnapshot == null) {
954 if (inputOptions.isEmpty()) {
956 inputOptions = "-threads 1 -skip_frame nokey -hide_banner";
958 inputOptions += " -threads 1 -skip_frame nokey -hide_banner";
960 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
961 cameraConfig.getSnapshotOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
962 + getThing().getUID().getId() + "/snapshot.jpg",
963 cameraConfig.getUser(), cameraConfig.getPassword());
965 Ffmpeg localSnaps = ffmpegSnapshot;
966 if (localSnaps != null) {
967 localSnaps.startConverting();
973 public void noMotionDetected(String thisAlarmsChannel) {
974 setChannelState(thisAlarmsChannel, OnOffType.OFF);
975 firstMotionAlarm = false;
976 motionAlarmUpdateSnapshot = false;
977 motionDetected = false;
978 if (streamingAutoFps) {
979 stopSnapshotPolling();
980 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
981 stopSnapshotPolling();
986 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
987 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
988 * tampering with the camera.
990 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
991 updateState(thisAlarmsChannel, state);
994 public void motionDetected(String thisAlarmsChannel) {
995 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
996 updateState(thisAlarmsChannel, OnOffType.ON);
997 motionDetected = true;
998 if (streamingAutoFps) {
999 startSnapshotPolling();
1001 if (cameraConfig.getUpdateImageWhen().contains("2")) {
1002 if (!firstMotionAlarm) {
1003 if (!snapshotUri.isEmpty()) {
1006 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1008 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1009 if (!snapshotPolling) {
1010 startSnapshotPolling();
1012 firstMotionAlarm = true;
1013 motionAlarmUpdateSnapshot = true;
1017 public void audioDetected() {
1018 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1019 if (cameraConfig.getUpdateImageWhen().contains("3")) {
1020 if (!firstAudioAlarm) {
1021 if (!snapshotUri.isEmpty()) {
1024 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1026 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1027 firstAudioAlarm = true;
1028 audioAlarmUpdateSnapshot = true;
1032 public void noAudioDetected() {
1033 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1034 firstAudioAlarm = false;
1035 audioAlarmUpdateSnapshot = false;
1038 public void recordMp4(String filename, int seconds) {
1039 mp4Filename = filename;
1040 mp4RecordTime = seconds;
1041 setupFfmpegFormat(FFmpegFormat.RECORD);
1042 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1045 public void recordGif(String filename, int seconds) {
1046 gifFilename = filename;
1047 gifRecordTime = seconds;
1048 if (cameraConfig.getGifPreroll() > 0) {
1049 snapCount = seconds;
1051 setupFfmpegFormat(FFmpegFormat.GIF);
1053 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1056 private void getReolinkToken() {
1057 sendHttpPOST("/api.cgi?cmd=Login",
1058 "[{\"cmd\":\"Login\", \"param\":{ \"User\":{ \"Version\": \"0\", \"userName\":\""
1059 + cameraConfig.getUser() + "\", \"password\":\"" + cameraConfig.getPassword() + "\"}}}]");
1062 public String returnValueFromString(String rawString, String searchedString) {
1064 int index = rawString.indexOf(searchedString);
1065 if (index != -1) // -1 means "not found"
1067 result = rawString.substring(index + searchedString.length(), rawString.length());
1068 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1070 return result; // Did not find a carriage return.
1072 return result.substring(0, index);
1075 return ""; // Did not find the String we were searching for
1078 private void sendPTZRequest() {
1079 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1083 public void channelLinked(ChannelUID channelUID) {
1084 switch (channelUID.getId()) {
1085 case CHANNEL_MJPEG_URL:
1086 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1087 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
1089 case CHANNEL_HLS_URL:
1090 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1091 + getThing().getUID().getId() + "/ipcamera.m3u8"));
1093 case CHANNEL_IMAGE_URL:
1094 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1095 + getThing().getUID().getId() + "/ipcamera.jpg"));
1100 public void removeChannels(List<org.openhab.core.thing.Channel> removeChannels) {
1101 if (!removeChannels.isEmpty()) {
1102 ThingBuilder thingBuilder = editThing();
1103 thingBuilder.withoutChannels(removeChannels);
1104 updateThing(thingBuilder.build());
1109 public void handleCommand(ChannelUID channelUID, Command command) {
1110 if (command instanceof RefreshType) {
1111 switch (channelUID.getId()) {
1113 if (onvifCamera.supportsPTZ()) {
1114 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1118 if (onvifCamera.supportsPTZ()) {
1119 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1123 if (onvifCamera.supportsPTZ()) {
1124 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1127 case CHANNEL_GOTO_PRESET:
1128 if (onvifCamera.supportsPTZ()) {
1129 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1133 } // caution "REFRESH" can still progress to brand Handlers below the else.
1135 switch (channelUID.getId()) {
1136 case CHANNEL_MP4_HISTORY_LENGTH:
1137 if (DecimalType.ZERO.equals(command)) {
1138 mp4HistoryLength = 0;
1140 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1143 case CHANNEL_GIF_HISTORY_LENGTH:
1144 if (DecimalType.ZERO.equals(command)) {
1145 gifHistoryLength = 0;
1147 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1150 case CHANNEL_FFMPEG_MOTION_CONTROL:
1151 if (OnOffType.ON.equals(command)) {
1152 ffmpegMotionAlarmEnabled = true;
1153 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1154 ffmpegMotionAlarmEnabled = false;
1155 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1156 } else if (command instanceof PercentType percentCommand) {
1157 ffmpegMotionAlarmEnabled = true;
1158 motionThreshold = percentCommand.toBigDecimal();
1160 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1162 case CHANNEL_START_STREAM:
1164 if (OnOffType.ON.equals(command)) {
1165 localHLS = ffmpegHLS;
1166 if (localHLS == null) {
1167 setupFfmpegFormat(FFmpegFormat.HLS);
1168 localHLS = ffmpegHLS;
1170 if (localHLS != null) {
1171 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1172 localHLS.startConverting();
1175 localHLS = ffmpegHLS;
1176 if (localHLS != null) {
1177 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1178 localHLS.setKeepAlive(1);
1182 case CHANNEL_EXTERNAL_MOTION:
1183 if (OnOffType.ON.equals(command)) {
1184 motionDetected(CHANNEL_EXTERNAL_MOTION);
1186 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1189 case CHANNEL_GOTO_PRESET:
1190 if (onvifCamera.supportsPTZ()) {
1191 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1194 case CHANNEL_POLL_IMAGE:
1195 if (OnOffType.ON.equals(command)) {
1196 if (snapshotUri.isEmpty()) {
1197 ffmpegSnapshotGeneration = true;
1198 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1199 updateImageChannel = false;
1201 updateImageChannel = true;
1202 updateSnapshot();// Allows this to change Image FPS on demand
1205 Ffmpeg localSnaps = ffmpegSnapshot;
1206 if (localSnaps != null) {
1207 localSnaps.stopConverting();
1208 ffmpegSnapshotGeneration = false;
1210 updateImageChannel = false;
1214 if (onvifCamera.supportsPTZ()) {
1215 if (command instanceof IncreaseDecreaseType) {
1216 if (command == IncreaseDecreaseType.INCREASE) {
1217 if (cameraConfig.getPtzContinuous()) {
1218 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1220 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1223 if (cameraConfig.getPtzContinuous()) {
1224 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1226 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1230 } else if (OnOffType.OFF.equals(command)) {
1231 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1234 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1235 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1239 if (onvifCamera.supportsPTZ()) {
1240 if (command instanceof IncreaseDecreaseType) {
1241 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1242 if (cameraConfig.getPtzContinuous()) {
1243 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1245 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1248 if (cameraConfig.getPtzContinuous()) {
1249 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1251 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1255 } else if (OnOffType.OFF.equals(command)) {
1256 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1259 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1260 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1264 if (onvifCamera.supportsPTZ()) {
1265 if (command instanceof IncreaseDecreaseType) {
1266 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1267 if (cameraConfig.getPtzContinuous()) {
1268 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1270 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1273 if (cameraConfig.getPtzContinuous()) {
1274 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1276 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1280 } else if (OnOffType.OFF.equals(command)) {
1281 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1284 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1285 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1290 // commands and refresh now get passed to brand handlers
1291 switch (thing.getThingTypeUID().getId()) {
1293 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1294 amcrestHandler.handleCommand(channelUID, command);
1295 if (lowPriorityRequests.isEmpty()) {
1296 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1300 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1301 dahuaHandler.handleCommand(channelUID, command);
1302 if (lowPriorityRequests.isEmpty()) {
1303 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1306 case DOORBIRD_THING:
1307 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1308 doorBirdHandler.handleCommand(channelUID, command);
1309 if (lowPriorityRequests.isEmpty()) {
1310 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1313 case HIKVISION_THING:
1314 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1315 hikvisionHandler.handleCommand(channelUID, command);
1318 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1319 cameraConfig.getPassword());
1320 foscamHandler.handleCommand(channelUID, command);
1321 if (lowPriorityRequests.isEmpty()) {
1322 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1326 InstarHandler instarHandler = new InstarHandler(getHandle());
1327 instarHandler.handleCommand(channelUID, command);
1328 if (lowPriorityRequests.isEmpty()) {
1329 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1333 ReolinkHandler reolinkHandler = new ReolinkHandler(getHandle());
1334 reolinkHandler.handleCommand(channelUID, command);
1337 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1338 defaultHandler.handleCommand(channelUID, command);
1339 if (lowPriorityRequests.isEmpty()) {
1340 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1346 public void setChannelState(String channelToUpdate, State valueOf) {
1347 updateState(channelToUpdate, valueOf);
1350 private void bringCameraOnline() {
1352 updateStatus(ThingStatus.ONLINE);
1353 groupTracker.listOfOnlineCameraHandlers.add(this);
1354 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1355 Future<?> localFuture = cameraConnectionJob;
1356 if (localFuture != null) {
1357 localFuture.cancel(false);
1358 cameraConnectionJob = null;
1360 if (!snapshotUri.isEmpty()) {
1361 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1362 snapshotPolling = true;
1363 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000,
1364 cameraConfig.getPollTime(), TimeUnit.MILLISECONDS);
1368 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1370 // auto restart mjpeg stream now camera is back online.
1371 CameraServlet localServlet = servlet;
1372 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1373 openCamerasStream();
1376 if (!rtspUri.isEmpty()) {
1377 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1379 if (updateImageChannel) {
1380 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1382 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1384 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1385 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1386 handle.cameraOnline(getThing().getUID().getId());
1391 void snapshotIsFfmpeg() {
1392 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1394 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1395 bringCameraOnline();
1396 if (!rtspUri.isEmpty()) {
1397 updateImageChannel = false;
1398 ffmpegSnapshotGeneration = true;
1399 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1400 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1402 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1407 * The {@link pollingCameraConnection} This polls to see if the camera is reachable only until the camera
1408 * successfully connects.
1412 void pollingCameraConnection() {
1414 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)
1415 || thing.getThingTypeUID().getId().equals(DOORBIRD_THING)) {
1416 if (rtspUri.isEmpty()) {
1417 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1419 if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1422 ffmpegSnapshotGeneration = false;
1427 if (cameraConfig.getOnvifPort() > 0 && !onvifCamera.isConnected()) {
1428 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP: {}:{}", cameraConfig.getIp(),
1429 cameraConfig.getOnvifPort());
1430 onvifCamera.connect(supportsOnvifEvents());
1433 if ("ffmpeg".equals(snapshotUri)) {
1435 } else if (!snapshotUri.isEmpty()) {
1436 ffmpegSnapshotGeneration = false;
1438 } else if (!rtspUri.isEmpty()) {
1441 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1442 "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.");
1446 public void cameraConfigError(String reason) {
1447 // won't try to reconnect again due to a config error being the cause.
1448 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1452 public void cameraCommunicationError(String reason) {
1453 // will try to reconnect again as camera may be rebooting.
1454 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1455 if (isOnline.get()) { // if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1456 resetAndRetryConnecting();
1460 private boolean streamIsStopped(String url) {
1461 ChannelTracking channelTracking = channelTrackingMap.get(url);
1462 if (channelTracking != null) {
1463 if (channelTracking.getChannel().isActive()) {
1464 return false; // stream is running.
1467 return true; // Stream stopped or never started.
1470 void snapshotRunnable() {
1471 // Snapshot should be first to keep consistent time between shots
1473 if (snapCount > 0) {
1474 if (--snapCount == 0) {
1475 setupFfmpegFormat(FFmpegFormat.GIF);
1480 private void takeSnapshot() {
1481 sendHttpGET(snapshotUri);
1484 private void updateSnapshot() {
1485 lastSnapshotRequest = Instant.now();
1486 mainEventLoopGroup.schedule(this::takeSnapshot, 0, TimeUnit.MILLISECONDS);
1489 public byte[] getSnapshot() {
1490 if (!isOnline.get()) {
1491 // Single gray pixel JPG to keep streams open when the camera goes offline so they dont stop.
1492 return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
1493 0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43,
1494 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
1495 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09,
1496 0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d,
1497 0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10,
1498 0x10, (byte) 0xff, (byte) 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
1499 (byte) 0xff, (byte) 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, (byte) 0xff, (byte) 0xda, 0x00, 0x08,
1500 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, (byte) 0xd2, (byte) 0xcf, 0x20, (byte) 0xff, (byte) 0xd9 };
1502 // Most cameras will return a 503 busy error if snapshot is faster than 1 second
1503 long lastUpdatedMs = Duration.between(lastSnapshotRequest, Instant.now()).toMillis();
1504 if (!snapshotPolling && !ffmpegSnapshotGeneration && lastUpdatedMs >= cameraConfig.getPollTime()) {
1507 lockCurrentSnapshot.lock();
1509 return currentSnapshot;
1511 lockCurrentSnapshot.unlock();
1515 public void stopSnapshotPolling() {
1516 Future<?> localFuture;
1517 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1518 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1519 snapshotPolling = false;
1520 localFuture = snapshotJob;
1521 if (localFuture != null) {
1522 localFuture.cancel(true);
1524 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1525 snapshotPolling = false;
1526 localFuture = snapshotJob;
1527 if (localFuture != null) {
1528 localFuture.cancel(true);
1533 public void startSnapshotPolling() {
1534 if (snapshotPolling || ffmpegSnapshotGeneration) {
1535 return; // Already polling or creating with FFmpeg from RTSP
1537 if (streamingSnapshotMjpeg || streamingAutoFps || cameraConfig.getUpdateImageWhen().contains("4")) {
1538 snapshotPolling = true;
1539 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 0, cameraConfig.getPollTime(),
1540 TimeUnit.MILLISECONDS);
1545 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep alarm
1546 * streams open and more.
1549 void pollCameraRunnable() {
1550 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1551 if (!lowPriorityRequests.isEmpty()) {
1552 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1553 lowPriorityCounter = 0;
1555 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1557 // what needs to be done every poll//
1558 switch (thing.getThingTypeUID().getId()) {
1560 checkCameraConnection();
1563 onvifCamera.sendOnvifRequest(RequestType.Renew, onvifCamera.subscriptionXAddr);
1564 if (onvifCamera.pullMessageRequests.intValue() == 0) {
1565 logger.info("The alarm stream was not running for ONVIF camera {}, re-starting it now",
1566 cameraConfig.getIp());
1567 onvifCamera.sendOnvifRequest(RequestType.PullMessages, onvifCamera.subscriptionXAddr);
1571 checkCameraConnection();
1572 noMotionDetected(CHANNEL_MOTION_ALARM);
1573 noMotionDetected(CHANNEL_PIR_ALARM);
1574 noMotionDetected(CHANNEL_HUMAN_ALARM);
1575 noMotionDetected(CHANNEL_CAR_ALARM);
1576 noMotionDetected(CHANNEL_ANIMAL_ALARM);
1579 case HIKVISION_THING:
1580 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1581 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1582 cameraConfig.getIp());
1583 sendHttpGET("/ISAPI/Event/notification/alertStream");
1587 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1588 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1591 if (cameraConfig.getOnvifPort() == 0) {
1592 sendHttpGET("/api.cgi?cmd=GetAiState&channel=" + cameraConfig.getNvrChannel() + reolinkAuth);
1593 sendHttpGET("/api.cgi?cmd=GetMdState&channel=" + cameraConfig.getNvrChannel() + reolinkAuth);
1595 onvifCamera.sendOnvifRequest(RequestType.Renew, onvifCamera.subscriptionXAddr);
1599 // Check for alarms, channel for NVRs appears not to work at filtering.
1600 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1601 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1602 cameraConfig.getIp());
1603 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1606 case DOORBIRD_THING:
1607 // Check for alarms, channel for NVRs appears not to work at filtering.
1608 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1609 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1610 cameraConfig.getIp());
1611 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1615 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1616 + cameraConfig.getPassword());
1619 Ffmpeg localFfmpeg = ffmpegHLS;
1620 if (localFfmpeg != null) {
1621 localFfmpeg.checkKeepAlive();
1623 if (ffmpegMotionAlarmEnabled || ffmpegAudioAlarmEnabled) {
1624 localFfmpeg = ffmpegRtspHelper;
1625 if (localFfmpeg == null || !localFfmpeg.isAlive()) {
1626 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1629 // check if the thread has frozen due to camera doing a soft reboot
1630 localFfmpeg = ffmpegMjpeg;
1631 if (localFfmpeg != null && !localFfmpeg.isAlive()) {
1632 logger.debug("MJPEG was not being produced by FFmpeg when it should have been, restarting FFmpeg.");
1633 setupFfmpegFormat(FFmpegFormat.MJPEG);
1635 if (openChannels.size() > 10) {
1636 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1642 public void initialize() {
1643 cameraConfig = getConfigAs(CameraConfig.class);
1644 threadPool = Executors.newScheduledThreadPool(2);
1645 mainEventLoopGroup = new NioEventLoopGroup(3);
1646 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1647 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1648 rtspUri = cameraConfig.getFfmpegInput();
1649 if (cameraConfig.getFfmpegOutput().isEmpty()) {
1651 .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1653 // Known cameras will connect quicker if we skip ONVIF questions.
1654 switch (thing.getThingTypeUID().getId()) {
1657 if (mjpegUri.isEmpty()) {
1658 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1660 if (snapshotUri.isEmpty()) {
1661 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1664 case DOORBIRD_THING:
1665 if (mjpegUri.isEmpty()) {
1666 mjpegUri = "/bha-api/video.cgi";
1668 if (snapshotUri.isEmpty()) {
1669 snapshotUri = "/bha-api/image.cgi";
1673 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1674 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1675 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1676 if (mjpegUri.isEmpty()) {
1677 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1678 + cameraConfig.getPassword();
1680 if (snapshotUri.isEmpty()) {
1681 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1682 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1685 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1686 if (mjpegUri.isEmpty()) {
1687 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1689 if (snapshotUri.isEmpty()) {
1690 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1692 if (lowPriorityRequests.isEmpty()) {
1693 lowPriorityRequests.add("/ISAPI/System/IO/capabilities");
1697 if (snapshotUri.isEmpty()) {
1698 snapshotUri = "/tmpfs/snap.jpg";
1700 if (mjpegUri.isEmpty()) {
1701 mjpegUri = "/mjpegstream.cgi?-chn=12";
1703 // Newer Instar cameras use this to setup the Alarm Server, plus it is used to work out which API is
1704 // implemented based on the response to these two requests.
1706 "/param.cgi?cmd=setasaction&-server=1&enable=1&-interval=1&cmd=setasattr&-as_index=1&-as_server="
1707 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1708 + getThing().getUID().getId()
1709 + "/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");
1710 // Older Instar cameras use this to setup the Alarm Server
1712 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
1713 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1714 + getThing().getUID().getId()
1715 + "/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");
1718 if (cameraConfig.useToken) {
1719 authenticationJob = threadPool.scheduleWithFixedDelay(this::getReolinkToken, 0, 45,
1722 reolinkAuth = "&user=" + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword();
1723 // The reply to api.cgi?cmd=Login also sends this only with a token
1724 sendHttpPOST("/api.cgi?cmd=GetAbility" + reolinkAuth,
1725 "[{ \"cmd\":\"GetAbility\", \"param\":{ \"User\":{ \"userName\":\"admin\" }}}]");
1727 if (snapshotUri.isEmpty()) {
1728 // ReolinkHandler will change the snapshotUri in the response to /api.cgi?cmd=Login
1729 snapshotUri = "/cgi-bin/api.cgi?cmd=Snap&channel=" + cameraConfig.getNvrChannel() + "&rs=openHAB"
1732 // channel numbers for snapshots start at 0, while the rtsp start at 1
1733 if (rtspUri.isEmpty()) {
1734 rtspUri = "rtsp://" + cameraConfig.getIp() + ":554/h264Preview_0"
1735 + (cameraConfig.getNvrChannel() + 1) + "_main";
1739 // for poll times 9 seconds and above don't display a warning about the Image channel.
1740 if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1742 "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.");
1744 // ONVIF and Instar event handling need the server started before connecting.
1745 startStreamServer();
1749 private void tryConnecting() {
1750 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)
1751 && !thing.getThingTypeUID().getId().equals(DOORBIRD_THING) && cameraConfig.getOnvifPort() > 0) {
1752 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1753 cameraConfig.getUser(), cameraConfig.getPassword());
1754 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1755 // Only use ONVIF events if it is not an API camera.
1756 onvifCamera.connect(supportsOnvifEvents());
1758 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 12, TimeUnit.SECONDS);
1761 private boolean supportsOnvifEvents() {
1762 switch (thing.getThingTypeUID().getId()) {
1766 if (cameraConfig.getOnvifPort() > 0) {
1773 private void keepMjpegRunning() {
1774 CameraServlet localServlet = servlet;
1775 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1776 if (!mjpegUri.isEmpty() && !"ffmpeg".equals(mjpegUri)) {
1777 localServlet.openStreams.queueFrame(("--" + localServlet.openStreams.boundary + "\r\n\r\n").getBytes());
1779 localServlet.openStreams.queueFrame(getSnapshot());
1783 // What the camera needs to re-connect if the initialize() is not called.
1784 private void resetAndRetryConnecting() {
1789 private void offline() {
1790 isOnline.set(false);
1791 snapshotPolling = false;
1792 Future<?> localFuture = pollCameraJob;
1793 if (localFuture != null) {
1794 localFuture.cancel(true);
1795 pollCameraJob = null;
1797 localFuture = authenticationJob;
1798 if (localFuture != null) {
1799 localFuture.cancel(true);
1800 authenticationJob = null;
1802 localFuture = snapshotJob;
1803 if (localFuture != null) {
1804 localFuture.cancel(true);
1807 localFuture = cameraConnectionJob;
1808 if (localFuture != null) {
1809 localFuture.cancel(true);
1810 cameraConnectionJob = null;
1812 Ffmpeg localFfmpeg = ffmpegHLS;
1813 if (localFfmpeg != null) {
1814 localFfmpeg.stopConverting();
1817 localFfmpeg = ffmpegRecord;
1818 if (localFfmpeg != null) {
1819 localFfmpeg.stopConverting();
1820 ffmpegRecord = null;
1822 localFfmpeg = ffmpegGIF;
1823 if (localFfmpeg != null) {
1824 localFfmpeg.stopConverting();
1827 localFfmpeg = ffmpegRtspHelper;
1828 if (localFfmpeg != null) {
1829 localFfmpeg.stopConverting();
1830 ffmpegRtspHelper = null;
1832 localFfmpeg = ffmpegMjpeg;
1833 if (localFfmpeg != null) {
1834 localFfmpeg.stopConverting();
1837 localFfmpeg = ffmpegSnapshot;
1838 if (localFfmpeg != null) {
1839 localFfmpeg.stopConverting();
1840 ffmpegSnapshot = null;
1842 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) { // generic cameras do not have ONVIF support
1843 onvifCamera.disconnect();
1845 openChannels.close();
1849 public void dispose() {
1851 CameraServlet localServlet = servlet;
1852 if (localServlet != null) {
1853 localServlet.dispose();
1856 threadPool.shutdown();
1857 // inform all group handlers that this camera has gone offline
1858 groupTracker.listOfOnlineCameraHandlers.remove(this);
1859 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1860 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1861 handle.cameraOffline(this);
1863 basicAuth = ""; // clear out stored Password hash
1864 useDigestAuth = false;
1865 mainEventLoopGroup.shutdownGracefully();
1866 mainBootstrap = null;
1867 channelTrackingMap.clear();
1870 public String getWhiteList() {
1871 return cameraConfig.getIpWhitelist();
1875 public Collection<Class<? extends ThingHandlerService>> getServices() {
1876 return Set.of(IpCameraActions.class);