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) { // Use RTSP stream creating snapshots to know camera is online.
518 Ffmpeg localSnapshot = ffmpegSnapshot;
519 if (localSnapshot != null && !localSnapshot.isAlive()) {
520 cameraCommunicationError("FFmpeg Snapshots Stopped: Check that your camera can be reached.");
522 return; // ffmpeg snapshot stream is still alive
525 // ONVIF cameras get regular event messages from the camera
526 if (supportsOnvifEvents() && onvifCamera.isConnected()) {
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 if (!snapshotPolling) {
1561 checkCameraConnection();
1565 if (!snapshotPolling) {
1566 checkCameraConnection();
1570 if (!snapshotPolling) {
1571 checkCameraConnection();
1573 noMotionDetected(CHANNEL_MOTION_ALARM);
1574 noMotionDetected(CHANNEL_PIR_ALARM);
1575 noMotionDetected(CHANNEL_HUMAN_ALARM);
1576 noMotionDetected(CHANNEL_CAR_ALARM);
1577 noMotionDetected(CHANNEL_ANIMAL_ALARM);
1580 case HIKVISION_THING:
1581 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1582 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1583 cameraConfig.getIp());
1584 sendHttpGET("/ISAPI/Event/notification/alertStream");
1588 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1589 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1592 if (cameraConfig.getNvrChannel() > 0) {
1593 sendHttpGET("/api.cgi?cmd=GetAiState&channel=" + cameraConfig.getNvrChannel() + "&user="
1594 + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword());
1595 sendHttpGET("/api.cgi?cmd=GetMdState&channel=" + cameraConfig.getNvrChannel() + "&user="
1596 + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword());
1597 } else if (!snapshotPolling) {
1598 checkCameraConnection();
1602 if (!snapshotPolling) {
1603 checkCameraConnection();
1605 // Check for alarms, channel for NVRs appears not to work at filtering.
1606 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1607 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1608 cameraConfig.getIp());
1609 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1612 case DOORBIRD_THING:
1613 if (!snapshotPolling) {
1614 checkCameraConnection();
1616 // Check for alarms, channel for NVRs appears not to work at filtering.
1617 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1618 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1619 cameraConfig.getIp());
1620 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1624 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1625 + cameraConfig.getPassword());
1628 Ffmpeg localFfmpeg = ffmpegHLS;
1629 if (localFfmpeg != null) {
1630 localFfmpeg.checkKeepAlive();
1632 if (ffmpegMotionAlarmEnabled || ffmpegAudioAlarmEnabled) {
1633 localFfmpeg = ffmpegRtspHelper;
1634 if (localFfmpeg == null || !localFfmpeg.isAlive()) {
1635 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1638 // check if the thread has frozen due to camera doing a soft reboot
1639 localFfmpeg = ffmpegMjpeg;
1640 if (localFfmpeg != null && !localFfmpeg.isAlive()) {
1641 logger.debug("MJPEG was not being produced by FFmpeg when it should have been, restarting FFmpeg.");
1642 setupFfmpegFormat(FFmpegFormat.MJPEG);
1644 if (openChannels.size() > 10) {
1645 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1651 public void initialize() {
1652 cameraConfig = getConfigAs(CameraConfig.class);
1653 threadPool = Executors.newScheduledThreadPool(2);
1654 mainEventLoopGroup = new NioEventLoopGroup(3);
1655 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1656 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1657 rtspUri = cameraConfig.getFfmpegInput();
1658 if (cameraConfig.getFfmpegOutput().isEmpty()) {
1660 .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1662 // Known cameras will connect quicker if we skip ONVIF questions.
1663 switch (thing.getThingTypeUID().getId()) {
1666 if (mjpegUri.isEmpty()) {
1667 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1669 if (snapshotUri.isEmpty()) {
1670 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1673 case DOORBIRD_THING:
1674 if (mjpegUri.isEmpty()) {
1675 mjpegUri = "/bha-api/video.cgi";
1677 if (snapshotUri.isEmpty()) {
1678 snapshotUri = "/bha-api/image.cgi";
1682 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1683 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1684 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1685 if (mjpegUri.isEmpty()) {
1686 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1687 + cameraConfig.getPassword();
1689 if (snapshotUri.isEmpty()) {
1690 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1691 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1694 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1695 if (mjpegUri.isEmpty()) {
1696 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1698 if (snapshotUri.isEmpty()) {
1699 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1701 if (lowPriorityRequests.isEmpty()) {
1702 lowPriorityRequests.add("/ISAPI/System/IO/capabilities");
1706 if (snapshotUri.isEmpty()) {
1707 snapshotUri = "/tmpfs/snap.jpg";
1709 if (mjpegUri.isEmpty()) {
1710 mjpegUri = "/mjpegstream.cgi?-chn=12";
1712 // Newer Instar cameras use this to setup the Alarm Server, plus it is used to work out which API is
1713 // implemented based on the response to these two requests.
1715 "/param.cgi?cmd=setasaction&-server=1&enable=1&-interval=1&cmd=setasattr&-as_index=1&-as_server="
1716 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1717 + getThing().getUID().getId()
1718 + "/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");
1719 // Older Instar cameras use this to setup the Alarm Server
1721 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
1722 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1723 + getThing().getUID().getId()
1724 + "/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");
1727 if (cameraConfig.useToken) {
1728 authenticationJob = threadPool.scheduleWithFixedDelay(this::getReolinkToken, 0, 45,
1731 reolinkAuth = "&user=" + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword();
1732 // The reply to api.cgi?cmd=Login also sends this only with a token
1733 sendHttpPOST("/api.cgi?cmd=GetAbility" + reolinkAuth,
1734 "[{ \"cmd\":\"GetAbility\", \"param\":{ \"User\":{ \"userName\":\"admin\" }}}]");
1736 if (snapshotUri.isEmpty()) {
1737 if (cameraConfig.getNvrChannel() < 1) {
1738 snapshotUri = "/cgi-bin/api.cgi?cmd=Snap&channel=0&rs=openHAB" + reolinkAuth;
1740 snapshotUri = "/cgi-bin/api.cgi?cmd=Snap&channel=" + (cameraConfig.getNvrChannel() - 1)
1741 + "&rs=openHAB" + reolinkAuth;
1744 if (rtspUri.isEmpty()) {
1745 if (cameraConfig.getNvrChannel() < 1) {
1746 rtspUri = "rtsp://" + cameraConfig.getIp() + ":554/h264Preview_01_main";
1748 rtspUri = "rtsp://" + cameraConfig.getIp() + ":554/h264Preview_0" + cameraConfig.getNvrChannel()
1754 // for poll times 9 seconds and above don't display a warning about the Image channel.
1755 if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1757 "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.");
1759 // ONVIF and Instar event handling need the server started before connecting.
1760 startStreamServer();
1764 private void tryConnecting() {
1765 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)
1766 && !thing.getThingTypeUID().getId().equals(DOORBIRD_THING) && cameraConfig.getOnvifPort() > 0) {
1767 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1768 cameraConfig.getUser(), cameraConfig.getPassword());
1769 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1770 // Only use ONVIF events if it is not an API camera.
1771 onvifCamera.connect(supportsOnvifEvents());
1773 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 12, TimeUnit.SECONDS);
1776 private boolean supportsOnvifEvents() {
1777 switch (thing.getThingTypeUID().getId()) {
1781 if (cameraConfig.getNvrChannel() < 1) {
1788 private void keepMjpegRunning() {
1789 CameraServlet localServlet = servlet;
1790 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1791 if (!mjpegUri.isEmpty() && !"ffmpeg".equals(mjpegUri)) {
1792 localServlet.openStreams.queueFrame(("--" + localServlet.openStreams.boundary + "\r\n\r\n").getBytes());
1794 localServlet.openStreams.queueFrame(getSnapshot());
1798 // What the camera needs to re-connect if the initialize() is not called.
1799 private void resetAndRetryConnecting() {
1804 private void offline() {
1805 isOnline.set(false);
1806 snapshotPolling = false;
1807 Future<?> localFuture = pollCameraJob;
1808 if (localFuture != null) {
1809 localFuture.cancel(true);
1810 pollCameraJob = null;
1812 localFuture = authenticationJob;
1813 if (localFuture != null) {
1814 localFuture.cancel(true);
1815 authenticationJob = null;
1817 localFuture = snapshotJob;
1818 if (localFuture != null) {
1819 localFuture.cancel(true);
1822 localFuture = cameraConnectionJob;
1823 if (localFuture != null) {
1824 localFuture.cancel(true);
1825 cameraConnectionJob = null;
1827 Ffmpeg localFfmpeg = ffmpegHLS;
1828 if (localFfmpeg != null) {
1829 localFfmpeg.stopConverting();
1832 localFfmpeg = ffmpegRecord;
1833 if (localFfmpeg != null) {
1834 localFfmpeg.stopConverting();
1835 ffmpegRecord = null;
1837 localFfmpeg = ffmpegGIF;
1838 if (localFfmpeg != null) {
1839 localFfmpeg.stopConverting();
1842 localFfmpeg = ffmpegRtspHelper;
1843 if (localFfmpeg != null) {
1844 localFfmpeg.stopConverting();
1845 ffmpegRtspHelper = null;
1847 localFfmpeg = ffmpegMjpeg;
1848 if (localFfmpeg != null) {
1849 localFfmpeg.stopConverting();
1852 localFfmpeg = ffmpegSnapshot;
1853 if (localFfmpeg != null) {
1854 localFfmpeg.stopConverting();
1855 ffmpegSnapshot = null;
1857 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) { // generic cameras do not have ONVIF support
1858 onvifCamera.disconnect();
1860 openChannels.close();
1864 public void dispose() {
1866 CameraServlet localServlet = servlet;
1867 if (localServlet != null) {
1868 localServlet.dispose();
1871 threadPool.shutdown();
1872 // inform all group handlers that this camera has gone offline
1873 groupTracker.listOfOnlineCameraHandlers.remove(this);
1874 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1875 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1876 handle.cameraOffline(this);
1878 basicAuth = ""; // clear out stored Password hash
1879 useDigestAuth = false;
1880 mainEventLoopGroup.shutdownGracefully();
1881 mainBootstrap = null;
1882 channelTrackingMap.clear();
1885 public String getWhiteList() {
1886 return cameraConfig.getIpWhitelist();
1890 public Collection<Class<? extends ThingHandlerService>> getServices() {
1891 return Set.of(IpCameraActions.class);