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.servlet.CameraServlet;
65 import org.openhab.core.OpenHAB;
66 import org.openhab.core.library.types.DecimalType;
67 import org.openhab.core.library.types.IncreaseDecreaseType;
68 import org.openhab.core.library.types.OnOffType;
69 import org.openhab.core.library.types.PercentType;
70 import org.openhab.core.library.types.RawType;
71 import org.openhab.core.library.types.StringType;
72 import org.openhab.core.thing.ChannelUID;
73 import org.openhab.core.thing.Thing;
74 import org.openhab.core.thing.ThingStatus;
75 import org.openhab.core.thing.ThingStatusDetail;
76 import org.openhab.core.thing.binding.BaseThingHandler;
77 import org.openhab.core.thing.binding.ThingHandlerService;
78 import org.openhab.core.thing.binding.builder.ThingBuilder;
79 import org.openhab.core.types.Command;
80 import org.openhab.core.types.RefreshType;
81 import org.openhab.core.types.State;
82 import org.osgi.framework.FrameworkUtil;
83 import org.osgi.service.http.HttpService;
84 import org.slf4j.Logger;
85 import org.slf4j.LoggerFactory;
87 import io.netty.bootstrap.Bootstrap;
88 import io.netty.buffer.ByteBuf;
89 import io.netty.buffer.Unpooled;
90 import io.netty.channel.Channel;
91 import io.netty.channel.ChannelDuplexHandler;
92 import io.netty.channel.ChannelFuture;
93 import io.netty.channel.ChannelFutureListener;
94 import io.netty.channel.ChannelHandlerContext;
95 import io.netty.channel.ChannelInitializer;
96 import io.netty.channel.ChannelOption;
97 import io.netty.channel.EventLoopGroup;
98 import io.netty.channel.group.ChannelGroup;
99 import io.netty.channel.group.DefaultChannelGroup;
100 import io.netty.channel.nio.NioEventLoopGroup;
101 import io.netty.channel.socket.SocketChannel;
102 import io.netty.channel.socket.nio.NioSocketChannel;
103 import io.netty.handler.codec.base64.Base64;
104 import io.netty.handler.codec.http.DefaultFullHttpRequest;
105 import io.netty.handler.codec.http.FullHttpRequest;
106 import io.netty.handler.codec.http.HttpClientCodec;
107 import io.netty.handler.codec.http.HttpContent;
108 import io.netty.handler.codec.http.HttpHeaderValues;
109 import io.netty.handler.codec.http.HttpMessage;
110 import io.netty.handler.codec.http.HttpMethod;
111 import io.netty.handler.codec.http.HttpResponse;
112 import io.netty.handler.codec.http.HttpVersion;
113 import io.netty.handler.codec.http.LastHttpContent;
114 import io.netty.handler.timeout.IdleState;
115 import io.netty.handler.timeout.IdleStateEvent;
116 import io.netty.handler.timeout.IdleStateHandler;
117 import io.netty.util.CharsetUtil;
118 import io.netty.util.ReferenceCountUtil;
119 import io.netty.util.concurrent.GlobalEventExecutor;
122 * The {@link IpCameraHandler} is responsible for handling commands, which are
123 * sent to one of the channels.
125 * @author Matthew Skinner - Initial contribution
128 public class IpCameraHandler extends BaseThingHandler {
129 public final Logger logger = LoggerFactory.getLogger(getClass());
130 public final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
131 private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2);
132 private GroupTracker groupTracker;
133 public CameraConfig cameraConfig = new CameraConfig();
135 // ChannelGroup is thread safe
136 public final ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
137 private final HttpService httpService;
138 private @Nullable CameraServlet servlet;
139 public String mjpegContentType = "";
140 public @Nullable Ffmpeg ffmpegHLS = null;
141 public @Nullable Ffmpeg ffmpegRecord = null;
142 public @Nullable Ffmpeg ffmpegGIF = null;
143 public @Nullable Ffmpeg ffmpegRtspHelper = null;
144 public @Nullable Ffmpeg ffmpegMjpeg = null;
145 public @Nullable Ffmpeg ffmpegSnapshot = null;
146 public boolean streamingAutoFps = false;
147 public boolean motionDetected = false;
148 public Instant lastSnapshotRequest = Instant.now();
149 public Instant currentSnapshotTime = Instant.now();
150 private @Nullable ScheduledFuture<?> cameraConnectionJob = null;
151 private @Nullable ScheduledFuture<?> pollCameraJob = null;
152 private @Nullable ScheduledFuture<?> snapshotJob = null;
153 private @Nullable ScheduledFuture<?> authenticationJob = null;
154 private @Nullable Bootstrap mainBootstrap;
155 private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup(1);
156 private FullHttpRequest putRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT, "");
157 private FullHttpRequest postRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "");
158 private String gifFilename = "ipcamera";
159 private String gifHistory = "";
160 private String mp4History = "";
161 public int gifHistoryLength;
162 public int mp4HistoryLength;
163 private String mp4Filename = "ipcamera";
164 private int mp4RecordTime;
165 private int gifRecordTime = 5;
166 private LinkedList<byte[]> fifoSnapshotBuffer = new LinkedList<>();
167 private int snapCount;
168 private boolean updateImageChannel = false;
169 private byte lowPriorityCounter = 0;
170 public String hostIp;
171 public Map<String, ChannelTracking> channelTrackingMap = new ConcurrentHashMap<>();
172 public List<String> lowPriorityRequests = new ArrayList<>(0);
174 // basicAuth MUST remain private as it holds the cameraConfig.getPassword()
175 private String basicAuth = "";
176 public String reolinkAuth = "&token=null";
177 public int reolinkScheduleVersion = 0;
178 public boolean useBasicAuth = false;
179 public boolean useDigestAuth = false;
180 public boolean newInstarApi = false;
181 public String snapshotUri = "";
182 public String mjpegUri = "";
183 private byte[] currentSnapshot = new byte[] { (byte) 0x00 };
184 public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
185 public String rtspUri = "";
186 public boolean audioAlarmUpdateSnapshot = false;
187 private boolean motionAlarmUpdateSnapshot = false;
188 private AtomicBoolean isOnline = new AtomicBoolean(); // Used so only 1 error is logged when a network issue occurs.
189 private boolean firstAudioAlarm = false;
190 private boolean firstMotionAlarm = false;
191 public BigDecimal motionThreshold = BigDecimal.ZERO;
192 public int audioThreshold = 35;
193 public boolean streamingSnapshotMjpeg = false;
194 public boolean ffmpegMotionAlarmEnabled = false;
195 public boolean ffmpegAudioAlarmEnabled = false;
196 public boolean ffmpegSnapshotGeneration = false;
197 public boolean snapshotPolling = false;
198 public OnvifConnection onvifCamera = new OnvifConnection(this, "", "", "");
200 // These methods handle the response from all camera brands, nothing specific to 1 brand.
201 private class CommonCameraHandler extends ChannelDuplexHandler {
202 private int bytesToRecieve = 0;
203 private int bytesAlreadyRecieved = 0;
204 private byte[] incomingJpeg = new byte[0];
205 private String incomingMessage = "";
206 private String contentType = "empty";
207 private String boundary = "";
208 private Object reply = new Object();
209 private String requestUrl = "";
210 private boolean isChunked = false;
212 public void setURL(String url) {
217 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
218 if (msg == null || ctx == null) {
222 if (msg instanceof HttpResponse response) {
223 if (response.status().code() == 200) {
224 if (!response.headers().isEmpty()) {
225 for (String name : response.headers().names()) {
226 // Some cameras use first letter uppercase and others dont.
227 switch (name.toLowerCase()) { // Possible localization issues doing this
229 contentType = response.headers().getAsString(name);
231 case "content-length":
232 bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
234 case "transfer-encoding":
235 if (response.headers().getAsString(name).contains("chunked")) {
241 if (contentType.contains("multipart")) {
242 boundary = Helper.searchString(contentType, "boundary=");
243 if (mjpegUri.equals(requestUrl)) {
244 if (msg instanceof HttpMessage) {
245 // very start of stream only
246 mjpegContentType = contentType;
247 CameraServlet localServlet = servlet;
248 if (localServlet != null) {
249 logger.debug("Setting Content-Type to: {}", contentType);
250 localServlet.openStreams.updateContentType(contentType, boundary);
254 } else if (contentType.contains("image/jp")) {
255 if (bytesToRecieve == 0) {
256 bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
257 logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
259 incomingJpeg = new byte[bytesToRecieve];
263 // Non 200 OK replies are logged and handled in pipeline by MyNettyAuthHandler.java
267 if (msg instanceof HttpContent content) {
268 if (mjpegUri.equals(requestUrl) && !(content instanceof LastHttpContent)) {
269 // multiple MJPEG stream packets come back as this.
270 byte[] chunkedFrame = new byte[content.content().readableBytes()];
271 content.content().getBytes(content.content().readerIndex(), chunkedFrame);
272 CameraServlet localServlet = servlet;
273 if (localServlet != null) {
274 localServlet.openStreams.queueFrame(chunkedFrame);
277 // Found some cameras use Content-Type: image/jpg instead of image/jpeg
278 if (contentType.contains("image/jp")) {
279 for (int i = 0; i < content.content().capacity(); i++) {
280 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
282 if (content instanceof LastHttpContent) {
283 processSnapshot(incomingJpeg);
286 } else { // incomingMessage that is not an IMAGE
287 if (incomingMessage.isEmpty()) {
288 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
290 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
292 bytesAlreadyRecieved = incomingMessage.length();
293 if (content instanceof LastHttpContent) {
294 // If it is not an image send it on to the next handler//
295 if (bytesAlreadyRecieved != 0) {
296 reply = incomingMessage;
297 super.channelRead(ctx, reply);
300 // Alarm Streams never have a LastHttpContent as they always stay open//
301 else if (contentType.contains("multipart")) {
302 int beginIndex, endIndex;
303 if (bytesToRecieve == 0) {
304 beginIndex = incomingMessage.indexOf("Content-Length:");
305 if (beginIndex != -1) {
306 endIndex = incomingMessage.indexOf("\r\n", beginIndex);
307 if (endIndex != -1) {
308 bytesToRecieve = Integer.parseInt(
309 incomingMessage.substring(beginIndex + 15, endIndex).strip());
313 // --boundary and headers are not included in the Content-Length value
314 if (bytesAlreadyRecieved > bytesToRecieve) {
315 // Check if message has a second --boundary
316 endIndex = incomingMessage.indexOf("--" + boundary, bytesToRecieve);
317 if (endIndex == -1) {
318 reply = incomingMessage;
319 incomingMessage = "";
321 bytesAlreadyRecieved = 0;
323 reply = incomingMessage.substring(0, endIndex);
324 incomingMessage = incomingMessage.substring(endIndex, incomingMessage.length());
325 bytesToRecieve = 0;// Triggers search next time for Content-Length:
326 bytesAlreadyRecieved = incomingMessage.length() - endIndex;
328 super.channelRead(ctx, reply);
331 // Foscam needs this as will other cameras with chunks//
332 if (isChunked && bytesAlreadyRecieved != 0) {
333 reply = incomingMessage;
337 } else { // msg is not HttpContent
338 // Foscam cameras need this
339 if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
340 reply = incomingMessage;
341 logger.trace("Packet back from camera is {}", incomingMessage);
342 super.channelRead(ctx, reply);
346 ReferenceCountUtil.release(msg);
351 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
352 if (cause == null || ctx == null) {
355 if (cause instanceof ArrayIndexOutOfBoundsException) {
356 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
359 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
366 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
370 if (evt instanceof IdleStateEvent e) {
371 // If camera does not use the channel for X amount of time it will close.
372 if (e.state() == IdleState.READER_IDLE) {
373 String urlToKeepOpen = "";
374 switch (thing.getThingTypeUID().getId()) {
376 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
379 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
382 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
383 if (channelTracking != null) {
384 if (channelTracking.getChannel().equals(ctx.channel())) {
385 return; // don't auto close this as it is for the alarms.
388 logger.debug("Closing an idle channel for camera: {}", cameraConfig.getIp());
395 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
396 IpCameraDynamicStateDescriptionProvider stateDescriptionProvider, HttpService httpService) {
398 this.stateDescriptionProvider = stateDescriptionProvider;
399 if (ipAddress != null) {
402 hostIp = Helper.getLocalIpAddress();
404 this.groupTracker = groupTracker;
405 this.httpService = httpService;
408 private IpCameraHandler getHandle() {
412 // false clears the stored user/pass hash, true creates the hash
413 public boolean setBasicAuth(boolean useBasic) {
415 logger.debug("Clearing out the stored BASIC auth now.");
418 } else if (!basicAuth.isEmpty()) {
419 // If the binding sends multiple requests before basicAuth was set, this may trigger falsely.
420 logger.warn("Camera is reporting your username and/or password is wrong.");
423 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
424 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
425 ByteBuf byteBuf = null;
427 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
428 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
430 if (byteBuf != null) {
436 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
441 public String getCorrectUrlFormat(String longUrl) {
442 String temp = longUrl;
445 if (longUrl.isEmpty() || "ffmpeg".equals(longUrl)) {
450 url = new URL(longUrl);
451 int port = url.getPort();
453 if (url.getQuery() == null) {
454 temp = url.getPath();
456 temp = url.getPath() + "?" + url.getQuery();
459 if (url.getQuery() == null) {
460 temp = ":" + url.getPort() + url.getPath();
462 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
465 } catch (MalformedURLException e) {
466 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
471 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
472 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
473 sendHttpRequest("PUT", httpRequestURL, null);
476 public void sendHttpPOST(String httpPostURL, String content) {
477 FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, httpPostURL);
478 request.headers().set("Host", cameraConfig.getIp());
479 request.headers().add("Content-Type", "application/json");
480 request.headers().add("User-Agent",
481 "openHAB/" + FrameworkUtil.getBundle(this.getClass()).getVersion().toString());
482 request.headers().add("Accept", "*/*");
483 ByteBuf bbuf = Unpooled.copiedBuffer(content, StandardCharsets.UTF_8);
484 request.headers().set("Content-Length", bbuf.readableBytes());
485 request.content().clear().writeBytes(bbuf);
486 postRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
487 sendHttpRequest("POST", httpPostURL, null);
490 public void sendHttpPOST(String httpRequestURL) {
491 sendHttpRequest("POST", httpRequestURL, null);
494 public void sendHttpGET(String httpRequestURL) {
495 sendHttpRequest("GET", httpRequestURL, null);
498 public int getPortFromShortenedUrl(String httpRequestURL) {
499 if (httpRequestURL.startsWith(":")) {
500 int end = httpRequestURL.indexOf("/");
501 return Integer.parseInt(httpRequestURL.substring(1, end));
503 return cameraConfig.getPort();
506 public String getTinyUrl(String httpRequestURL) {
507 if (httpRequestURL.startsWith(":")) {
508 int beginIndex = httpRequestURL.indexOf("/");
509 return httpRequestURL.substring(beginIndex);
511 return httpRequestURL;
514 private void checkCameraConnection() {
515 if (snapshotPolling) { // Currently polling a real URL for snapshots, so camera must be online.
517 } else if (ffmpegSnapshotGeneration) {
518 Ffmpeg localSnapshot = ffmpegSnapshot;
519 if (localSnapshot != null && !localSnapshot.isAlive()) {
520 cameraCommunicationError("FFmpeg Snapshots Stopped: Check that your camera can be reached.");
522 return; // RTSP stream is creating snapshots, so camera is online.
525 if (supportsOnvifEvents() && onvifCamera.isConnected() && onvifCamera.getEventsSupported()) {
526 return;// ONVIF cameras that are getting event messages must be online
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("Connection Timeout: Check your IP:" + cameraConfig.getIp() + " and PORT:"
540 + cameraConfig.getPort() + " 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);
549 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port, httpRequestURL);
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().addLast(HIKVISION_HANDLER,
587 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);
640 if (cameraConnectionJob != null && !isOnline.get()) {
643 openChannel(ch, httpRequestURL);
644 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
645 commonHandler.setURL(httpRequestURLFull);
646 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
647 authHandler.setURL(httpMethod, httpRequestURL);
649 switch (thing.getThingTypeUID().getId()) {
651 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
652 amcrestHandler.setURL(httpRequestURL);
654 case HIKVISION_THING:
655 HikvisionHandler hikvisionHandler = (HikvisionHandler) ch.pipeline()
656 .get(HIKVISION_HANDLER);
657 hikvisionHandler.setURL(httpRequestURL);
660 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
661 instarHandler.setURL(httpRequestURL);
664 ReolinkHandler reolinkHandler = (ReolinkHandler) ch.pipeline().get(REOLINK_HANDLER);
665 reolinkHandler.setURL(httpRequestURL);
668 ch.writeAndFlush(request);
669 } else { // an error occurred
670 cameraCommunicationError(
671 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
677 public void processSnapshot(byte[] incommingSnapshot) {
678 lockCurrentSnapshot.lock();
680 currentSnapshot = incommingSnapshot;
681 if (cameraConfig.getGifPreroll() > 0) {
682 fifoSnapshotBuffer.add(incommingSnapshot);
683 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
684 fifoSnapshotBuffer.removeFirst();
688 lockCurrentSnapshot.unlock();
689 currentSnapshotTime = Instant.now();
692 if (updateImageChannel) {
693 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
694 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
695 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
696 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
697 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
698 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
699 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
703 public void startStreamServer() {
704 servlet = new CameraServlet(this, httpService);
705 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
706 + getThing().getUID().getId() + "/ipcamera.m3u8"));
707 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
708 + getThing().getUID().getId() + "/ipcamera.jpg"));
709 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
710 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
713 public void openCamerasStream() {
714 if (mjpegUri.isEmpty() || "ffmpeg".equals(mjpegUri)) {
715 setupFfmpegFormat(FFmpegFormat.MJPEG);
718 closeChannel(getTinyUrl(mjpegUri));
719 // Dahua cameras crash if you refresh (close and open) the stream without this delay.
720 mainEventLoopGroup.schedule(this::openMjpegStream, 300, TimeUnit.MILLISECONDS);
723 private void openMjpegStream() {
724 sendHttpGET(mjpegUri);
727 private void openChannel(Channel channel, String httpRequestURL) {
728 ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
729 if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
730 tracker.setChannel(channel);
733 channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
736 public void closeChannel(String url) {
737 ChannelTracking channelTracking = channelTrackingMap.get(url);
738 if (channelTracking != null) {
739 if (channelTracking.getChannel().isOpen()) {
740 channelTracking.getChannel().close();
747 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
748 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
751 private void cleanChannels() {
752 for (Channel channel : openChannels) {
753 boolean oldChannel = true;
754 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
755 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
756 channelTrackingMap.remove(channelTracking.getRequestUrl());
758 if (channelTracking.getChannel().equals(channel)) {
759 logger.debug("Open channel to camera is used for URL: {}", channelTracking.getRequestUrl());
769 public void storeHttpReply(String url, String content) {
770 ChannelTracking channelTracking = channelTrackingMap.get(url);
771 if (channelTracking != null) {
772 channelTracking.setReply(content);
776 private void storeSnapshots() {
778 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
779 lockCurrentSnapshot.lock();
781 for (byte[] foo : fifoSnapshotBuffer) {
782 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
785 OutputStream fos = new FileOutputStream(file);
788 } catch (FileNotFoundException e) {
789 logger.warn("FileNotFoundException {}", e.getMessage());
790 } catch (IOException e) {
791 logger.warn("IOException {}", e.getMessage());
795 lockCurrentSnapshot.unlock();
799 public void setupFfmpegFormat(FFmpegFormat format) {
800 String inputOptions = cameraConfig.getFfmpegInputOptions();
801 if (cameraConfig.getFfmpegOutput().isEmpty()) {
802 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
805 if (rtspUri.isEmpty()) {
806 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
809 if (cameraConfig.getFfmpegLocation().isEmpty()) {
810 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
813 if (rtspUri.toLowerCase().contains("rtsp")) {
814 if (inputOptions.isEmpty()) {
815 inputOptions = "-rtsp_transport tcp";
819 // Make sure the folder exists, if not create it.
820 new File(cameraConfig.getFfmpegOutput()).mkdirs();
823 if (ffmpegHLS == null) {
824 if (!inputOptions.isEmpty()) {
825 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
826 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
827 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
828 cameraConfig.getUser(), cameraConfig.getPassword());
830 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
831 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
832 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
833 cameraConfig.getPassword());
836 Ffmpeg localHLS = ffmpegHLS;
837 if (localHLS != null) {
838 localHLS.startConverting();
842 if (cameraConfig.getGifPreroll() > 0) {
843 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
844 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
845 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
846 + cameraConfig.getGifOutOptions(),
847 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
848 cameraConfig.getPassword());
850 if (!inputOptions.isEmpty()) {
851 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
853 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
855 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
856 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
857 cameraConfig.getUser(), cameraConfig.getPassword());
859 if (cameraConfig.getGifPreroll() > 0) {
862 Ffmpeg localGIF = ffmpegGIF;
863 if (localGIF != null) {
864 localGIF.startConverting();
865 if (gifHistory.isEmpty()) {
866 gifHistory = gifFilename;
867 } else if (!"ipcamera".equals(gifFilename)) {
868 gifHistory = gifFilename + "," + gifHistory;
869 if (gifHistoryLength > 49) {
870 int endIndex = gifHistory.lastIndexOf(",");
871 gifHistory = gifHistory.substring(0, endIndex);
874 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
878 if (!inputOptions.isEmpty()) {
879 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
881 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
883 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
884 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
885 cameraConfig.getUser(), cameraConfig.getPassword());
886 ffmpegRecord.startConverting();
887 if (mp4History.isEmpty()) {
888 mp4History = mp4Filename;
889 } else if (!"ipcamera".equals(mp4Filename)) {
890 mp4History = mp4Filename + "," + mp4History;
891 if (mp4HistoryLength > 49) {
892 int endIndex = mp4History.lastIndexOf(",");
893 mp4History = mp4History.substring(0, endIndex);
896 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
899 Ffmpeg localAlarms = ffmpegRtspHelper;
900 if (localAlarms != null) {
901 localAlarms.stopConverting();
902 if (!ffmpegAudioAlarmEnabled && !ffmpegMotionAlarmEnabled) {
906 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
907 String filterOptions = "";
908 if (!ffmpegAudioAlarmEnabled) {
909 filterOptions = "-an";
911 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
913 if (!ffmpegMotionAlarmEnabled && !ffmpegSnapshotGeneration) {
914 filterOptions = filterOptions.concat(" -vn");
915 } else if (ffmpegMotionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
916 String usersMotionOptions = cameraConfig.getMotionOptions();
917 if (usersMotionOptions.startsWith("-")) {
918 // Need to put the users custom options first in the chain before the motion is detected
919 filterOptions += " " + usersMotionOptions + ",select='gte(scene,"
920 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
922 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
923 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
925 } else if (ffmpegMotionAlarmEnabled) {
926 filterOptions = filterOptions.concat(" -vf select='gte(scene,"
927 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print");
929 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
930 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
931 ffmpegRtspHelper.startConverting();
934 if (ffmpegMjpeg == null) {
935 if (inputOptions.isEmpty()) {
936 inputOptions = "-hide_banner";
938 inputOptions += " -hide_banner";
940 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
941 cameraConfig.getMjpegOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
942 + getThing().getUID().getId() + "/ipcamera.jpg",
943 cameraConfig.getUser(), cameraConfig.getPassword());
945 Ffmpeg localMjpeg = ffmpegMjpeg;
946 if (localMjpeg != null) {
947 localMjpeg.startConverting();
951 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
952 if (ffmpegSnapshot == null) {
953 if (inputOptions.isEmpty()) {
955 inputOptions = "-threads 1 -skip_frame nokey -hide_banner";
957 inputOptions += " -threads 1 -skip_frame nokey -hide_banner";
959 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
960 cameraConfig.getSnapshotOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
961 + getThing().getUID().getId() + "/snapshot.jpg",
962 cameraConfig.getUser(), cameraConfig.getPassword());
964 Ffmpeg localSnaps = ffmpegSnapshot;
965 if (localSnaps != null) {
966 localSnaps.startConverting();
972 public void noMotionDetected(String thisAlarmsChannel) {
973 setChannelState(thisAlarmsChannel, OnOffType.OFF);
974 firstMotionAlarm = false;
975 motionAlarmUpdateSnapshot = false;
976 motionDetected = false;
977 if (streamingAutoFps) {
978 stopSnapshotPolling();
979 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
980 stopSnapshotPolling();
985 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
986 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
987 * tampering with the camera.
989 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
990 updateState(thisAlarmsChannel, state);
993 public void motionDetected(String thisAlarmsChannel) {
994 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
995 updateState(thisAlarmsChannel, OnOffType.ON);
996 motionDetected = true;
997 if (streamingAutoFps) {
998 startSnapshotPolling();
1000 if (cameraConfig.getUpdateImageWhen().contains("2")) {
1001 if (!firstMotionAlarm) {
1002 if (!snapshotUri.isEmpty()) {
1005 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1007 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1008 if (!snapshotPolling) {
1009 startSnapshotPolling();
1011 firstMotionAlarm = true;
1012 motionAlarmUpdateSnapshot = true;
1016 public void audioDetected() {
1017 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1018 if (cameraConfig.getUpdateImageWhen().contains("3")) {
1019 if (!firstAudioAlarm) {
1020 if (!snapshotUri.isEmpty()) {
1023 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1025 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1026 firstAudioAlarm = true;
1027 audioAlarmUpdateSnapshot = true;
1031 public void noAudioDetected() {
1032 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1033 firstAudioAlarm = false;
1034 audioAlarmUpdateSnapshot = false;
1037 public void recordMp4(String filename, int seconds) {
1038 mp4Filename = filename;
1039 mp4RecordTime = seconds;
1040 setupFfmpegFormat(FFmpegFormat.RECORD);
1041 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1044 public void recordGif(String filename, int seconds) {
1045 gifFilename = filename;
1046 gifRecordTime = seconds;
1047 if (cameraConfig.getGifPreroll() > 0) {
1048 snapCount = seconds;
1050 setupFfmpegFormat(FFmpegFormat.GIF);
1052 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1055 private void getReolinkToken() {
1056 sendHttpPOST("/api.cgi?cmd=Login",
1057 "[{\"cmd\":\"Login\", \"param\":{ \"User\":{ \"Version\": \"0\", \"userName\":\""
1058 + cameraConfig.getUser() + "\", \"password\":\"" + cameraConfig.getPassword() + "\"}}}]");
1061 public String returnValueFromString(String rawString, String searchedString) {
1063 int index = rawString.indexOf(searchedString);
1064 if (index != -1) // -1 means "not found"
1066 result = rawString.substring(index + searchedString.length(), rawString.length());
1067 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1069 return result; // Did not find a carriage return.
1071 return result.substring(0, index);
1074 return ""; // Did not find the String we were searching for
1077 private void sendPTZRequest() {
1078 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1082 public void channelLinked(ChannelUID channelUID) {
1083 switch (channelUID.getId()) {
1084 case CHANNEL_MJPEG_URL:
1085 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1086 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
1088 case CHANNEL_HLS_URL:
1089 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1090 + getThing().getUID().getId() + "/ipcamera.m3u8"));
1092 case CHANNEL_IMAGE_URL:
1093 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1094 + getThing().getUID().getId() + "/ipcamera.jpg"));
1099 public void removeChannels(List<org.openhab.core.thing.Channel> removeChannels) {
1100 if (!removeChannels.isEmpty()) {
1101 ThingBuilder thingBuilder = editThing();
1102 thingBuilder.withoutChannels(removeChannels);
1103 updateThing(thingBuilder.build());
1108 public void handleCommand(ChannelUID channelUID, Command command) {
1109 if (command instanceof RefreshType) {
1110 switch (channelUID.getId()) {
1112 if (onvifCamera.supportsPTZ()) {
1113 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1117 if (onvifCamera.supportsPTZ()) {
1118 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1122 if (onvifCamera.supportsPTZ()) {
1123 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1126 case CHANNEL_GOTO_PRESET:
1127 if (onvifCamera.supportsPTZ()) {
1128 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1132 } // caution "REFRESH" can still progress to brand Handlers below the else.
1134 switch (channelUID.getId()) {
1135 case CHANNEL_MP4_HISTORY_LENGTH:
1136 if (DecimalType.ZERO.equals(command)) {
1137 mp4HistoryLength = 0;
1139 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1142 case CHANNEL_GIF_HISTORY_LENGTH:
1143 if (DecimalType.ZERO.equals(command)) {
1144 gifHistoryLength = 0;
1146 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1149 case CHANNEL_FFMPEG_MOTION_CONTROL:
1150 if (OnOffType.ON.equals(command)) {
1151 ffmpegMotionAlarmEnabled = true;
1152 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1153 ffmpegMotionAlarmEnabled = false;
1154 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1155 } else if (command instanceof PercentType percentCommand) {
1156 ffmpegMotionAlarmEnabled = true;
1157 motionThreshold = percentCommand.toBigDecimal();
1159 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1161 case CHANNEL_START_STREAM:
1163 if (OnOffType.ON.equals(command)) {
1164 localHLS = ffmpegHLS;
1165 if (localHLS == null) {
1166 setupFfmpegFormat(FFmpegFormat.HLS);
1167 localHLS = ffmpegHLS;
1169 if (localHLS != null) {
1170 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1171 localHLS.startConverting();
1174 localHLS = ffmpegHLS;
1175 if (localHLS != null) {
1176 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1177 localHLS.setKeepAlive(1);
1181 case CHANNEL_EXTERNAL_MOTION:
1182 if (OnOffType.ON.equals(command)) {
1183 motionDetected(CHANNEL_EXTERNAL_MOTION);
1185 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1188 case CHANNEL_GOTO_PRESET:
1189 if (onvifCamera.supportsPTZ()) {
1190 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1193 case CHANNEL_POLL_IMAGE:
1194 if (OnOffType.ON.equals(command)) {
1195 if (snapshotUri.isEmpty()) {
1196 ffmpegSnapshotGeneration = true;
1197 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1198 updateImageChannel = false;
1200 updateImageChannel = true;
1201 updateSnapshot();// Allows this to change Image FPS on demand
1204 Ffmpeg localSnaps = ffmpegSnapshot;
1205 if (localSnaps != null) {
1206 localSnaps.stopConverting();
1207 ffmpegSnapshotGeneration = false;
1209 updateImageChannel = false;
1213 if (onvifCamera.supportsPTZ()) {
1214 if (command instanceof IncreaseDecreaseType) {
1215 if (command == IncreaseDecreaseType.INCREASE) {
1216 if (cameraConfig.getPtzContinuous()) {
1217 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1219 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1222 if (cameraConfig.getPtzContinuous()) {
1223 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1225 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1229 } else if (OnOffType.OFF.equals(command)) {
1230 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1233 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1234 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1238 if (onvifCamera.supportsPTZ()) {
1239 if (command instanceof IncreaseDecreaseType) {
1240 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1241 if (cameraConfig.getPtzContinuous()) {
1242 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1244 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1247 if (cameraConfig.getPtzContinuous()) {
1248 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1250 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1254 } else if (OnOffType.OFF.equals(command)) {
1255 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1258 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1259 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1263 if (onvifCamera.supportsPTZ()) {
1264 if (command instanceof IncreaseDecreaseType) {
1265 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1266 if (cameraConfig.getPtzContinuous()) {
1267 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1269 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1272 if (cameraConfig.getPtzContinuous()) {
1273 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1275 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1279 } else if (OnOffType.OFF.equals(command)) {
1280 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1283 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1284 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1289 // commands and refresh now get passed to brand handlers
1290 switch (thing.getThingTypeUID().getId()) {
1292 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1293 amcrestHandler.handleCommand(channelUID, command);
1294 if (lowPriorityRequests.isEmpty()) {
1295 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1299 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1300 dahuaHandler.handleCommand(channelUID, command);
1301 if (lowPriorityRequests.isEmpty()) {
1302 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1305 case DOORBIRD_THING:
1306 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1307 doorBirdHandler.handleCommand(channelUID, command);
1308 if (lowPriorityRequests.isEmpty()) {
1309 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1312 case HIKVISION_THING:
1313 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1314 hikvisionHandler.handleCommand(channelUID, command);
1317 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1318 cameraConfig.getPassword());
1319 foscamHandler.handleCommand(channelUID, command);
1320 if (lowPriorityRequests.isEmpty()) {
1321 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1325 InstarHandler instarHandler = new InstarHandler(getHandle());
1326 instarHandler.handleCommand(channelUID, command);
1327 if (lowPriorityRequests.isEmpty()) {
1328 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1332 ReolinkHandler reolinkHandler = new ReolinkHandler(getHandle());
1333 reolinkHandler.handleCommand(channelUID, command);
1336 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1337 defaultHandler.handleCommand(channelUID, command);
1338 if (lowPriorityRequests.isEmpty()) {
1339 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1345 public void setChannelState(String channelToUpdate, State valueOf) {
1346 updateState(channelToUpdate, valueOf);
1349 private void bringCameraOnline() {
1351 updateStatus(ThingStatus.ONLINE);
1352 groupTracker.listOfOnlineCameraHandlers.add(this);
1353 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1354 Future<?> localFuture = cameraConnectionJob;
1355 if (localFuture != null) {
1356 localFuture.cancel(false);
1357 cameraConnectionJob = null;
1359 if (!snapshotUri.isEmpty()) {
1360 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1361 snapshotPolling = true;
1362 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000,
1363 cameraConfig.getPollTime(), TimeUnit.MILLISECONDS);
1367 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1369 // auto restart mjpeg stream now camera is back online.
1370 CameraServlet localServlet = servlet;
1371 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1372 openCamerasStream();
1375 if (!rtspUri.isEmpty()) {
1376 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1378 if (updateImageChannel) {
1379 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1381 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1383 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1384 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1385 handle.cameraOnline(getThing().getUID().getId());
1390 void snapshotIsFfmpeg() {
1391 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1393 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1394 bringCameraOnline();
1395 if (!rtspUri.isEmpty()) {
1396 updateImageChannel = false;
1397 ffmpegSnapshotGeneration = true;
1398 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1399 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1401 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1406 * The {@link pollingCameraConnection} This polls to see if the camera is reachable only until the camera
1407 * successfully connects.
1411 void pollingCameraConnection() {
1413 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)
1414 || thing.getThingTypeUID().getId().equals(DOORBIRD_THING)) {
1415 if (rtspUri.isEmpty()) {
1416 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1418 if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1421 ffmpegSnapshotGeneration = false;
1426 if (cameraConfig.getOnvifPort() > 0 && !onvifCamera.isConnected()) {
1427 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP: {}:{}", cameraConfig.getIp(),
1428 cameraConfig.getOnvifPort());
1429 onvifCamera.connect(supportsOnvifEvents());
1432 if ("ffmpeg".equals(snapshotUri)) {
1434 } else if (!snapshotUri.isEmpty()) {
1435 ffmpegSnapshotGeneration = false;
1437 } else if (!rtspUri.isEmpty()) {
1440 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1441 "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.");
1445 public void cameraConfigError(String reason) {
1446 // won't try to reconnect again due to a config error being the cause.
1447 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1451 public void cameraCommunicationError(String reason) {
1452 // will try to reconnect again as camera may be rebooting.
1453 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1454 if (isOnline.get()) { // if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1455 resetAndRetryConnecting();
1459 private boolean streamIsStopped(String url) {
1460 ChannelTracking channelTracking = channelTrackingMap.get(url);
1461 if (channelTracking != null) {
1462 if (channelTracking.getChannel().isActive()) {
1463 return false; // stream is running.
1466 return true; // Stream stopped or never started.
1469 void snapshotRunnable() {
1470 // Snapshot should be first to keep consistent time between shots
1472 if (snapCount > 0) {
1473 if (--snapCount == 0) {
1474 setupFfmpegFormat(FFmpegFormat.GIF);
1479 private void takeSnapshot() {
1480 sendHttpGET(snapshotUri);
1483 private void updateSnapshot() {
1484 lastSnapshotRequest = Instant.now();
1485 mainEventLoopGroup.schedule(this::takeSnapshot, 0, TimeUnit.MILLISECONDS);
1488 public byte[] getSnapshot() {
1489 if (!isOnline.get()) {
1490 // Single gray pixel JPG to keep streams open when the camera goes offline so they dont stop.
1491 return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
1492 0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43,
1493 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
1494 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09,
1495 0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d,
1496 0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10,
1497 0x10, (byte) 0xff, (byte) 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
1498 (byte) 0xff, (byte) 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, (byte) 0xff, (byte) 0xda, 0x00, 0x08,
1499 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, (byte) 0xd2, (byte) 0xcf, 0x20, (byte) 0xff, (byte) 0xd9 };
1501 // Most cameras will return a 503 busy error if snapshot is faster than 1 second
1502 long lastUpdatedMs = Duration.between(lastSnapshotRequest, Instant.now()).toMillis();
1503 if (!snapshotPolling && !ffmpegSnapshotGeneration && lastUpdatedMs >= cameraConfig.getPollTime()) {
1506 lockCurrentSnapshot.lock();
1508 return currentSnapshot;
1510 lockCurrentSnapshot.unlock();
1514 public void stopSnapshotPolling() {
1515 Future<?> localFuture;
1516 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1517 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1518 snapshotPolling = false;
1519 localFuture = snapshotJob;
1520 if (localFuture != null) {
1521 localFuture.cancel(true);
1523 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1524 snapshotPolling = false;
1525 localFuture = snapshotJob;
1526 if (localFuture != null) {
1527 localFuture.cancel(true);
1532 public void startSnapshotPolling() {
1533 if (snapshotPolling || ffmpegSnapshotGeneration) {
1534 return; // Already polling or creating with FFmpeg from RTSP
1536 if (streamingSnapshotMjpeg || streamingAutoFps || cameraConfig.getUpdateImageWhen().contains("4")) {
1537 snapshotPolling = true;
1538 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 0, cameraConfig.getPollTime(),
1539 TimeUnit.MILLISECONDS);
1544 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep alarm
1545 * streams open and more.
1548 void pollCameraRunnable() {
1549 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1550 if (!lowPriorityRequests.isEmpty()) {
1551 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1552 lowPriorityCounter = 0;
1554 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1556 // what needs to be done every poll//
1557 switch (thing.getThingTypeUID().getId()) {
1559 if (!snapshotPolling) {
1560 checkCameraConnection();
1564 if (!snapshotPolling) {
1565 checkCameraConnection();
1569 if (!snapshotPolling) {
1570 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.getNvrChannel() > 0) {
1592 sendHttpGET("/api.cgi?cmd=GetAiState&channel=" + cameraConfig.getNvrChannel() + "&user="
1593 + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword());
1594 sendHttpGET("/api.cgi?cmd=GetMdState&channel=" + cameraConfig.getNvrChannel() + "&user="
1595 + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword());
1596 } else if (!snapshotPolling) {
1597 checkCameraConnection();
1601 if (!snapshotPolling) {
1602 checkCameraConnection();
1604 // Check for alarms, channel for NVRs appears not to work at filtering.
1605 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1606 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1607 cameraConfig.getIp());
1608 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1611 case DOORBIRD_THING:
1612 if (!snapshotPolling) {
1613 checkCameraConnection();
1615 // Check for alarms, channel for NVRs appears not to work at filtering.
1616 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1617 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1618 cameraConfig.getIp());
1619 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1623 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1624 + cameraConfig.getPassword());
1627 Ffmpeg localFfmpeg = ffmpegHLS;
1628 if (localFfmpeg != null) {
1629 localFfmpeg.checkKeepAlive();
1631 if (ffmpegMotionAlarmEnabled || ffmpegAudioAlarmEnabled) {
1632 localFfmpeg = ffmpegRtspHelper;
1633 if (localFfmpeg == null || !localFfmpeg.isAlive()) {
1634 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1637 // check if the thread has frozen due to camera doing a soft reboot
1638 localFfmpeg = ffmpegMjpeg;
1639 if (localFfmpeg != null && !localFfmpeg.isAlive()) {
1640 logger.debug("MJPEG was not being produced by FFmpeg when it should have been, restarting FFmpeg.");
1641 setupFfmpegFormat(FFmpegFormat.MJPEG);
1643 if (openChannels.size() > 10) {
1644 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1650 public void initialize() {
1651 cameraConfig = getConfigAs(CameraConfig.class);
1652 threadPool = Executors.newScheduledThreadPool(2);
1653 mainEventLoopGroup = new NioEventLoopGroup(3);
1654 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1655 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1656 rtspUri = cameraConfig.getFfmpegInput();
1657 if (cameraConfig.getFfmpegOutput().isEmpty()) {
1659 .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1661 // Known cameras will connect quicker if we skip ONVIF questions.
1662 switch (thing.getThingTypeUID().getId()) {
1665 if (mjpegUri.isEmpty()) {
1666 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1668 if (snapshotUri.isEmpty()) {
1669 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1672 case DOORBIRD_THING:
1673 if (mjpegUri.isEmpty()) {
1674 mjpegUri = "/bha-api/video.cgi";
1676 if (snapshotUri.isEmpty()) {
1677 snapshotUri = "/bha-api/image.cgi";
1681 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1682 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1683 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1684 if (mjpegUri.isEmpty()) {
1685 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1686 + cameraConfig.getPassword();
1688 if (snapshotUri.isEmpty()) {
1689 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1690 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1693 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1694 if (mjpegUri.isEmpty()) {
1695 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1697 if (snapshotUri.isEmpty()) {
1698 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1700 if (lowPriorityRequests.isEmpty()) {
1701 lowPriorityRequests.add("/ISAPI/System/IO/capabilities");
1705 if (snapshotUri.isEmpty()) {
1706 snapshotUri = "/tmpfs/snap.jpg";
1708 if (mjpegUri.isEmpty()) {
1709 mjpegUri = "/mjpegstream.cgi?-chn=12";
1711 // Newer Instar cameras use this to setup the Alarm Server, plus it is used to work out which API is
1712 // implemented based on the response to these two requests.
1714 "/param.cgi?cmd=setasaction&-server=1&enable=1&-interval=1&cmd=setasattr&-as_index=1&-as_server="
1715 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1716 + getThing().getUID().getId()
1717 + "/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");
1718 // Older Instar cameras use this to setup the Alarm Server
1720 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
1721 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1722 + getThing().getUID().getId()
1723 + "/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");
1726 if (cameraConfig.useToken) {
1727 authenticationJob = threadPool.scheduleWithFixedDelay(this::getReolinkToken, 0, 45,
1730 reolinkAuth = "&user=" + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword();
1731 // The reply to api.cgi?cmd=Login also sends this only with a token
1732 sendHttpPOST("/api.cgi?cmd=GetAbility" + reolinkAuth,
1733 "[{ \"cmd\":\"GetAbility\", \"param\":{ \"User\":{ \"userName\":\"admin\" }}}]");
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() {
1764 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)
1765 && !thing.getThingTypeUID().getId().equals(DOORBIRD_THING) && cameraConfig.getOnvifPort() > 0) {
1766 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1767 cameraConfig.getUser(), cameraConfig.getPassword());
1768 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1769 // Only use ONVIF events if it is not an API camera.
1770 onvifCamera.connect(supportsOnvifEvents());
1772 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 12, TimeUnit.SECONDS);
1775 private boolean supportsOnvifEvents() {
1776 switch (thing.getThingTypeUID().getId()) {
1780 if (cameraConfig.getNvrChannel() < 1) {
1787 private void keepMjpegRunning() {
1788 CameraServlet localServlet = servlet;
1789 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1790 if (!mjpegUri.isEmpty() && !"ffmpeg".equals(mjpegUri)) {
1791 localServlet.openStreams.queueFrame(("--" + localServlet.openStreams.boundary + "\r\n\r\n").getBytes());
1793 localServlet.openStreams.queueFrame(getSnapshot());
1797 // What the camera needs to re-connect if the initialize() is not called.
1798 private void resetAndRetryConnecting() {
1803 private void offline() {
1804 isOnline.set(false);
1805 snapshotPolling = false;
1806 Future<?> localFuture = pollCameraJob;
1807 if (localFuture != null) {
1808 localFuture.cancel(true);
1809 pollCameraJob = null;
1811 localFuture = authenticationJob;
1812 if (localFuture != null) {
1813 localFuture.cancel(true);
1814 authenticationJob = null;
1816 localFuture = snapshotJob;
1817 if (localFuture != null) {
1818 localFuture.cancel(true);
1821 localFuture = cameraConnectionJob;
1822 if (localFuture != null) {
1823 localFuture.cancel(true);
1824 cameraConnectionJob = null;
1826 Ffmpeg localFfmpeg = ffmpegHLS;
1827 if (localFfmpeg != null) {
1828 localFfmpeg.stopConverting();
1831 localFfmpeg = ffmpegRecord;
1832 if (localFfmpeg != null) {
1833 localFfmpeg.stopConverting();
1834 ffmpegRecord = null;
1836 localFfmpeg = ffmpegGIF;
1837 if (localFfmpeg != null) {
1838 localFfmpeg.stopConverting();
1841 localFfmpeg = ffmpegRtspHelper;
1842 if (localFfmpeg != null) {
1843 localFfmpeg.stopConverting();
1844 ffmpegRtspHelper = null;
1846 localFfmpeg = ffmpegMjpeg;
1847 if (localFfmpeg != null) {
1848 localFfmpeg.stopConverting();
1851 localFfmpeg = ffmpegSnapshot;
1852 if (localFfmpeg != null) {
1853 localFfmpeg.stopConverting();
1854 ffmpegSnapshot = null;
1856 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) { // generic cameras do not have ONVIF support
1857 onvifCamera.disconnect();
1859 openChannels.close();
1863 public void dispose() {
1865 CameraServlet localServlet = servlet;
1866 if (localServlet != null) {
1867 localServlet.dispose();
1870 threadPool.shutdown();
1871 // inform all group handlers that this camera has gone offline
1872 groupTracker.listOfOnlineCameraHandlers.remove(this);
1873 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1874 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1875 handle.cameraOffline(this);
1877 basicAuth = ""; // clear out stored Password hash
1878 useDigestAuth = false;
1879 mainEventLoopGroup.shutdownGracefully();
1880 mainBootstrap = null;
1881 channelTrackingMap.clear();
1884 public String getWhiteList() {
1885 return cameraConfig.getIpWhitelist();
1889 public Collection<Class<? extends ThingHandlerService>> getServices() {
1890 return Set.of(IpCameraActions.class);