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.locks.ReentrantLock;
43 import org.eclipse.jdt.annotation.NonNullByDefault;
44 import org.eclipse.jdt.annotation.Nullable;
45 import org.openhab.binding.ipcamera.internal.AmcrestHandler;
46 import org.openhab.binding.ipcamera.internal.CameraConfig;
47 import org.openhab.binding.ipcamera.internal.ChannelTracking;
48 import org.openhab.binding.ipcamera.internal.DahuaHandler;
49 import org.openhab.binding.ipcamera.internal.DoorBirdHandler;
50 import org.openhab.binding.ipcamera.internal.Ffmpeg;
51 import org.openhab.binding.ipcamera.internal.FoscamHandler;
52 import org.openhab.binding.ipcamera.internal.GroupTracker;
53 import org.openhab.binding.ipcamera.internal.Helper;
54 import org.openhab.binding.ipcamera.internal.HikvisionHandler;
55 import org.openhab.binding.ipcamera.internal.HttpOnlyHandler;
56 import org.openhab.binding.ipcamera.internal.InstarHandler;
57 import org.openhab.binding.ipcamera.internal.IpCameraActions;
58 import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
59 import org.openhab.binding.ipcamera.internal.IpCameraDynamicStateDescriptionProvider;
60 import org.openhab.binding.ipcamera.internal.MyNettyAuthHandler;
61 import org.openhab.binding.ipcamera.internal.ReolinkHandler;
62 import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection;
63 import org.openhab.binding.ipcamera.internal.servlet.CameraServlet;
64 import org.openhab.core.OpenHAB;
65 import org.openhab.core.library.types.DecimalType;
66 import org.openhab.core.library.types.IncreaseDecreaseType;
67 import org.openhab.core.library.types.OnOffType;
68 import org.openhab.core.library.types.PercentType;
69 import org.openhab.core.library.types.RawType;
70 import org.openhab.core.library.types.StringType;
71 import org.openhab.core.thing.ChannelUID;
72 import org.openhab.core.thing.Thing;
73 import org.openhab.core.thing.ThingStatus;
74 import org.openhab.core.thing.ThingStatusDetail;
75 import org.openhab.core.thing.binding.BaseThingHandler;
76 import org.openhab.core.thing.binding.ThingHandlerService;
77 import org.openhab.core.thing.binding.builder.ThingBuilder;
78 import org.openhab.core.types.Command;
79 import org.openhab.core.types.RefreshType;
80 import org.openhab.core.types.State;
81 import org.osgi.framework.FrameworkUtil;
82 import org.osgi.service.http.HttpService;
83 import org.slf4j.Logger;
84 import org.slf4j.LoggerFactory;
86 import io.netty.bootstrap.Bootstrap;
87 import io.netty.buffer.ByteBuf;
88 import io.netty.buffer.Unpooled;
89 import io.netty.channel.Channel;
90 import io.netty.channel.ChannelDuplexHandler;
91 import io.netty.channel.ChannelFuture;
92 import io.netty.channel.ChannelFutureListener;
93 import io.netty.channel.ChannelHandlerContext;
94 import io.netty.channel.ChannelInitializer;
95 import io.netty.channel.ChannelOption;
96 import io.netty.channel.EventLoopGroup;
97 import io.netty.channel.group.ChannelGroup;
98 import io.netty.channel.group.DefaultChannelGroup;
99 import io.netty.channel.nio.NioEventLoopGroup;
100 import io.netty.channel.socket.SocketChannel;
101 import io.netty.channel.socket.nio.NioSocketChannel;
102 import io.netty.handler.codec.base64.Base64;
103 import io.netty.handler.codec.http.DefaultFullHttpRequest;
104 import io.netty.handler.codec.http.FullHttpRequest;
105 import io.netty.handler.codec.http.HttpClientCodec;
106 import io.netty.handler.codec.http.HttpContent;
107 import io.netty.handler.codec.http.HttpHeaderValues;
108 import io.netty.handler.codec.http.HttpMessage;
109 import io.netty.handler.codec.http.HttpMethod;
110 import io.netty.handler.codec.http.HttpResponse;
111 import io.netty.handler.codec.http.HttpVersion;
112 import io.netty.handler.codec.http.LastHttpContent;
113 import io.netty.handler.timeout.IdleState;
114 import io.netty.handler.timeout.IdleStateEvent;
115 import io.netty.handler.timeout.IdleStateHandler;
116 import io.netty.util.CharsetUtil;
117 import io.netty.util.ReferenceCountUtil;
118 import io.netty.util.concurrent.GlobalEventExecutor;
121 * The {@link IpCameraHandler} is responsible for handling commands, which are
122 * sent to one of the channels.
124 * @author Matthew Skinner - Initial contribution
127 public class IpCameraHandler extends BaseThingHandler {
128 public final Logger logger = LoggerFactory.getLogger(getClass());
129 public final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
130 private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2);
131 private GroupTracker groupTracker;
132 public CameraConfig cameraConfig = new CameraConfig();
134 // ChannelGroup is thread safe
135 public final ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
136 private final HttpService httpService;
137 private @Nullable CameraServlet servlet;
138 public String mjpegContentType = "";
139 public @Nullable Ffmpeg ffmpegHLS = null;
140 public @Nullable Ffmpeg ffmpegRecord = null;
141 public @Nullable Ffmpeg ffmpegGIF = null;
142 public @Nullable Ffmpeg ffmpegRtspHelper = null;
143 public @Nullable Ffmpeg ffmpegMjpeg = null;
144 public @Nullable Ffmpeg ffmpegSnapshot = null;
145 public boolean streamingAutoFps = false;
146 public boolean motionDetected = false;
147 public Instant lastSnapshotRequest = Instant.now();
148 public Instant currentSnapshotTime = Instant.now();
149 private @Nullable ScheduledFuture<?> cameraConnectionJob = null;
150 private @Nullable ScheduledFuture<?> pollCameraJob = null;
151 private @Nullable ScheduledFuture<?> snapshotJob = null;
152 private @Nullable ScheduledFuture<?> authenticationJob = null;
153 private @Nullable Bootstrap mainBootstrap;
154 private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup(1);
155 private FullHttpRequest putRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT, "");
156 private FullHttpRequest postRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "");
157 private String gifFilename = "ipcamera";
158 private String gifHistory = "";
159 private String mp4History = "";
160 public int gifHistoryLength;
161 public int mp4HistoryLength;
162 private String mp4Filename = "ipcamera";
163 private int mp4RecordTime;
164 private int gifRecordTime = 5;
165 private LinkedList<byte[]> fifoSnapshotBuffer = new LinkedList<byte[]>();
166 private int snapCount;
167 private boolean updateImageChannel = false;
168 private byte lowPriorityCounter = 0;
169 public String hostIp;
170 public Map<String, ChannelTracking> channelTrackingMap = new ConcurrentHashMap<>();
171 public List<String> lowPriorityRequests = new ArrayList<>(0);
173 // basicAuth MUST remain private as it holds the cameraConfig.getPassword()
174 private String basicAuth = "";
175 public String reolinkAuth = "&token=null";
176 public boolean useBasicAuth = false;
177 public boolean useDigestAuth = false;
178 public boolean newInstarApi = false;
179 public String snapshotUri = "";
180 public String mjpegUri = "";
181 private byte[] currentSnapshot = new byte[] { (byte) 0x00 };
182 public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
183 public String rtspUri = "";
184 public boolean audioAlarmUpdateSnapshot = false;
185 private boolean motionAlarmUpdateSnapshot = false;
186 private boolean isOnline = false; // Used so only 1 error is logged when a network issue occurs.
187 private boolean firstAudioAlarm = false;
188 private boolean firstMotionAlarm = false;
189 public BigDecimal motionThreshold = BigDecimal.ZERO;
190 public int audioThreshold = 35;
191 public boolean streamingSnapshotMjpeg = false;
192 public boolean ffmpegMotionAlarmEnabled = false;
193 public boolean ffmpegAudioAlarmEnabled = false;
194 public boolean ffmpegSnapshotGeneration = false;
195 public boolean snapshotPolling = false;
196 public OnvifConnection onvifCamera = new OnvifConnection(this, "", "", "");
198 // These methods handle the response from all camera brands, nothing specific to 1 brand.
199 private class CommonCameraHandler extends ChannelDuplexHandler {
200 private int bytesToRecieve = 0;
201 private int bytesAlreadyRecieved = 0;
202 private byte[] incomingJpeg = new byte[0];
203 private String incomingMessage = "";
204 private String contentType = "empty";
205 private String boundary = "";
206 private Object reply = new Object();
207 private String requestUrl = "";
208 private boolean isChunked = false;
210 public void setURL(String url) {
215 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
216 if (msg == null || ctx == null) {
220 if (msg instanceof HttpResponse response) {
221 if (response.status().code() == 200) {
222 if (!response.headers().isEmpty()) {
223 for (String name : response.headers().names()) {
224 // Some cameras use first letter uppercase and others dont.
225 switch (name.toLowerCase()) { // Possible localization issues doing this
227 contentType = response.headers().getAsString(name);
229 case "content-length":
230 bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
232 case "transfer-encoding":
233 if (response.headers().getAsString(name).contains("chunked")) {
239 if (contentType.contains("multipart")) {
240 boundary = Helper.searchString(contentType, "boundary=");
241 if (mjpegUri.equals(requestUrl)) {
242 if (msg instanceof HttpMessage) {
243 // very start of stream only
244 mjpegContentType = contentType;
245 CameraServlet localServlet = servlet;
246 if (localServlet != null) {
247 logger.debug("Setting Content-Type to: {}", contentType);
248 localServlet.openStreams.updateContentType(contentType, boundary);
252 } else if (contentType.contains("image/jp")) {
253 if (bytesToRecieve == 0) {
254 bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
255 logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
257 incomingJpeg = new byte[bytesToRecieve];
261 // Non 200 OK replies are logged and handled in pipeline by MyNettyAuthHandler.java
265 if (msg instanceof HttpContent content) {
266 if (mjpegUri.equals(requestUrl) && !(content instanceof LastHttpContent)) {
267 // multiple MJPEG stream packets come back as this.
268 byte[] chunkedFrame = new byte[content.content().readableBytes()];
269 content.content().getBytes(content.content().readerIndex(), chunkedFrame);
270 CameraServlet localServlet = servlet;
271 if (localServlet != null) {
272 localServlet.openStreams.queueFrame(chunkedFrame);
275 // Found some cameras use Content-Type: image/jpg instead of image/jpeg
276 if (contentType.contains("image/jp")) {
277 for (int i = 0; i < content.content().capacity(); i++) {
278 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
280 if (content instanceof LastHttpContent) {
281 processSnapshot(incomingJpeg);
284 } else { // incomingMessage that is not an IMAGE
285 if (incomingMessage.isEmpty()) {
286 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
288 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
290 bytesAlreadyRecieved = incomingMessage.length();
291 if (content instanceof LastHttpContent) {
292 // If it is not an image send it on to the next handler//
293 if (bytesAlreadyRecieved != 0) {
294 reply = incomingMessage;
295 super.channelRead(ctx, reply);
298 // Alarm Streams never have a LastHttpContent as they always stay open//
299 else if (contentType.contains("multipart")) {
300 int beginIndex, endIndex;
301 if (bytesToRecieve == 0) {
302 beginIndex = incomingMessage.indexOf("Content-Length:");
303 if (beginIndex != -1) {
304 endIndex = incomingMessage.indexOf("\r\n", beginIndex);
305 if (endIndex != -1) {
306 bytesToRecieve = Integer.parseInt(
307 incomingMessage.substring(beginIndex + 15, endIndex).strip());
311 // --boundary and headers are not included in the Content-Length value
312 if (bytesAlreadyRecieved > bytesToRecieve) {
313 // Check if message has a second --boundary
314 endIndex = incomingMessage.indexOf("--" + boundary, bytesToRecieve);
315 if (endIndex == -1) {
316 reply = incomingMessage;
317 incomingMessage = "";
319 bytesAlreadyRecieved = 0;
321 reply = incomingMessage.substring(0, endIndex);
322 incomingMessage = incomingMessage.substring(endIndex, incomingMessage.length());
323 bytesToRecieve = 0;// Triggers search next time for Content-Length:
324 bytesAlreadyRecieved = incomingMessage.length() - endIndex;
326 super.channelRead(ctx, reply);
329 // Foscam needs this as will other cameras with chunks//
330 if (isChunked && bytesAlreadyRecieved != 0) {
331 reply = incomingMessage;
335 } else { // msg is not HttpContent
336 // Foscam cameras need this
337 if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
338 reply = incomingMessage;
339 logger.trace("Packet back from camera is {}", incomingMessage);
340 super.channelRead(ctx, reply);
344 ReferenceCountUtil.release(msg);
349 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
350 if (cause == null || ctx == null) {
353 if (cause instanceof ArrayIndexOutOfBoundsException) {
354 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
357 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
364 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
368 if (evt instanceof IdleStateEvent e) {
369 // If camera does not use the channel for X amount of time it will close.
370 if (e.state() == IdleState.READER_IDLE) {
371 String urlToKeepOpen = "";
372 switch (thing.getThingTypeUID().getId()) {
374 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
377 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
380 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
381 if (channelTracking != null) {
382 if (channelTracking.getChannel().equals(ctx.channel())) {
383 return; // don't auto close this as it is for the alarms.
386 logger.debug("Closing an idle channel for camera: {}", cameraConfig.getIp());
393 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
394 IpCameraDynamicStateDescriptionProvider stateDescriptionProvider, HttpService httpService) {
396 this.stateDescriptionProvider = stateDescriptionProvider;
397 if (ipAddress != null) {
400 hostIp = Helper.getLocalIpAddress();
402 this.groupTracker = groupTracker;
403 this.httpService = httpService;
406 private IpCameraHandler getHandle() {
410 // false clears the stored user/pass hash, true creates the hash
411 public boolean setBasicAuth(boolean useBasic) {
413 logger.debug("Clearing out the stored BASIC auth now.");
416 } else if (!basicAuth.isEmpty()) {
417 // If the binding sends multiple requests before basicAuth was set, this may trigger falsely.
418 logger.warn("Camera is reporting your username and/or password is wrong.");
421 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
422 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
423 ByteBuf byteBuf = null;
425 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
426 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
428 if (byteBuf != null) {
434 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
439 private String getCorrectUrlFormat(String longUrl) {
440 String temp = longUrl;
443 if (longUrl.isEmpty() || "ffmpeg".equals(longUrl)) {
448 url = new URL(longUrl);
449 int port = url.getPort();
451 if (url.getQuery() == null) {
452 temp = url.getPath();
454 temp = url.getPath() + "?" + url.getQuery();
457 if (url.getQuery() == null) {
458 temp = ":" + url.getPort() + url.getPath();
460 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
463 } catch (MalformedURLException e) {
464 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
469 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
470 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
471 sendHttpRequest("PUT", httpRequestURL, null);
474 public void sendHttpPOST(String httpPostURL, String content) {
475 FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, httpPostURL);
476 request.headers().set("Host", cameraConfig.getIp());
477 request.headers().add("Content-Type", "application/json");
478 request.headers().add("User-Agent",
479 "openHAB/" + FrameworkUtil.getBundle(this.getClass()).getVersion().toString());
480 request.headers().add("Accept", "*/*");
481 ByteBuf bbuf = Unpooled.copiedBuffer(content, StandardCharsets.UTF_8);
482 request.headers().set("Content-Length", bbuf.readableBytes());
483 request.content().clear().writeBytes(bbuf);
484 postRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
485 sendHttpRequest("POST", httpPostURL, null);
488 public void sendHttpPOST(String httpRequestURL) {
489 sendHttpRequest("POST", httpRequestURL, null);
492 public void sendHttpGET(String httpRequestURL) {
493 sendHttpRequest("GET", httpRequestURL, null);
496 public int getPortFromShortenedUrl(String httpRequestURL) {
497 if (httpRequestURL.startsWith(":")) {
498 int end = httpRequestURL.indexOf("/");
499 return Integer.parseInt(httpRequestURL.substring(1, end));
501 return cameraConfig.getPort();
504 public String getTinyUrl(String httpRequestURL) {
505 if (httpRequestURL.startsWith(":")) {
506 int beginIndex = httpRequestURL.indexOf("/");
507 return httpRequestURL.substring(beginIndex);
509 return httpRequestURL;
512 private void checkCameraConnection() {
513 if (snapshotPolling) { // Currently polling a real URL for snapshots, so camera must be online.
515 } else if (ffmpegSnapshotGeneration) { // Use RTSP stream creating snapshots to know camera is online.
516 Ffmpeg localSnapshot = ffmpegSnapshot;
517 if (localSnapshot != null && !localSnapshot.isAlive()) {
518 cameraCommunicationError("FFmpeg Snapshots Stopped: Check that your camera can be reached.");
521 return; // ffmpeg snapshot stream is still alive
524 // if ONVIF cam also use connection state which is updated by regular messages to camera
525 if (thing.getThingTypeUID().getId().equals(ONVIF_THING) && snapshotUri.isEmpty() && onvifCamera.isConnected()) {
529 // Open a HTTP connection without sending any requests as we do not need a snapshot.
530 Bootstrap localBootstrap = mainBootstrap;
531 if (localBootstrap != null) {
532 ChannelFuture chFuture = localBootstrap
533 .connect(new InetSocketAddress(cameraConfig.getIp(), cameraConfig.getPort()));
534 if (chFuture.awaitUninterruptibly(500)) {
535 chFuture.channel().close();
539 cameraCommunicationError(
540 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
543 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
544 // The authHandler will generate a digest string and re-send using this same function when needed.
545 @SuppressWarnings("null")
546 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
547 int port = getPortFromShortenedUrl(httpRequestURLFull);
548 String httpRequestURL = getTinyUrl(httpRequestURLFull);
550 if (mainBootstrap == null) {
551 mainBootstrap = new Bootstrap();
552 mainBootstrap.group(mainEventLoopGroup);
553 mainBootstrap.channel(NioSocketChannel.class);
554 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
555 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
556 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
557 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
558 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
559 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
562 public void initChannel(SocketChannel socketChannel) throws Exception {
563 // HIK Alarm stream needs > 9sec idle to stop stream closing
564 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
565 socketChannel.pipeline().addLast(new HttpClientCodec());
566 socketChannel.pipeline().addLast(AUTH_HANDLER,
567 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
568 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
570 switch (thing.getThingTypeUID().getId()) {
572 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
575 socketChannel.pipeline()
576 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
579 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
582 socketChannel.pipeline().addLast(
583 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
585 case HIKVISION_THING:
586 socketChannel.pipeline().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);
643 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
646 openChannel(ch, httpRequestURL);
647 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
648 commonHandler.setURL(httpRequestURLFull);
649 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
650 authHandler.setURL(httpMethod, httpRequestURL);
652 switch (thing.getThingTypeUID().getId()) {
654 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
655 amcrestHandler.setURL(httpRequestURL);
657 case HIKVISION_THING:
658 HikvisionHandler hikvisionHandler = (HikvisionHandler) ch.pipeline()
659 .get(HIKVISION_HANDLER);
660 hikvisionHandler.setURL(httpRequestURL);
663 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
664 instarHandler.setURL(httpRequestURL);
667 ReolinkHandler reolinkHandler = (ReolinkHandler) ch.pipeline().get(REOLINK_HANDLER);
668 reolinkHandler.setURL(httpRequestURL);
671 ch.writeAndFlush(request);
672 } else { // an error occurred
673 cameraCommunicationError(
674 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
680 public void processSnapshot(byte[] incommingSnapshot) {
681 lockCurrentSnapshot.lock();
683 currentSnapshot = incommingSnapshot;
684 if (cameraConfig.getGifPreroll() > 0) {
685 fifoSnapshotBuffer.add(incommingSnapshot);
686 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
687 fifoSnapshotBuffer.removeFirst();
691 lockCurrentSnapshot.unlock();
692 currentSnapshotTime = Instant.now();
695 if (updateImageChannel) {
696 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
697 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
698 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
699 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
700 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
701 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
702 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
706 public void startStreamServer() {
707 servlet = new CameraServlet(this, httpService);
708 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
709 + getThing().getUID().getId() + "/ipcamera.m3u8"));
710 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
711 + getThing().getUID().getId() + "/ipcamera.jpg"));
712 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
713 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
716 public void openCamerasStream() {
717 if (mjpegUri.isEmpty() || "ffmpeg".equals(mjpegUri)) {
718 setupFfmpegFormat(FFmpegFormat.MJPEG);
721 closeChannel(getTinyUrl(mjpegUri));
722 // Dahua cameras crash if you refresh (close and open) the stream without this delay.
723 mainEventLoopGroup.schedule(this::openMjpegStream, 300, TimeUnit.MILLISECONDS);
726 private void openMjpegStream() {
727 sendHttpGET(mjpegUri);
730 private void openChannel(Channel channel, String httpRequestURL) {
731 ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
732 if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
733 tracker.setChannel(channel);
736 channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
739 public void closeChannel(String url) {
740 ChannelTracking channelTracking = channelTrackingMap.get(url);
741 if (channelTracking != null) {
742 if (channelTracking.getChannel().isOpen()) {
743 channelTracking.getChannel().close();
750 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
751 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
754 private void cleanChannels() {
755 for (Channel channel : openChannels) {
756 boolean oldChannel = true;
757 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
758 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
759 channelTrackingMap.remove(channelTracking.getRequestUrl());
761 if (channelTracking.getChannel().equals(channel)) {
762 logger.debug("Open channel to camera is used for URL: {}", channelTracking.getRequestUrl());
772 public void storeHttpReply(String url, String content) {
773 ChannelTracking channelTracking = channelTrackingMap.get(url);
774 if (channelTracking != null) {
775 channelTracking.setReply(content);
779 private void storeSnapshots() {
781 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
782 lockCurrentSnapshot.lock();
784 for (byte[] foo : fifoSnapshotBuffer) {
785 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
788 OutputStream fos = new FileOutputStream(file);
791 } catch (FileNotFoundException e) {
792 logger.warn("FileNotFoundException {}", e.getMessage());
793 } catch (IOException e) {
794 logger.warn("IOException {}", e.getMessage());
798 lockCurrentSnapshot.unlock();
802 public void setupFfmpegFormat(FFmpegFormat format) {
803 String inputOptions = cameraConfig.getFfmpegInputOptions();
804 if (cameraConfig.getFfmpegOutput().isEmpty()) {
805 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
808 if (rtspUri.isEmpty()) {
809 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
812 if (cameraConfig.getFfmpegLocation().isEmpty()) {
813 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
816 if (rtspUri.toLowerCase().contains("rtsp")) {
817 if (inputOptions.isEmpty()) {
818 inputOptions = "-rtsp_transport tcp";
822 // Make sure the folder exists, if not create it.
823 new File(cameraConfig.getFfmpegOutput()).mkdirs();
826 if (ffmpegHLS == null) {
827 if (!inputOptions.isEmpty()) {
828 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
829 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
830 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
831 cameraConfig.getUser(), cameraConfig.getPassword());
833 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
834 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
835 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
836 cameraConfig.getPassword());
839 Ffmpeg localHLS = ffmpegHLS;
840 if (localHLS != null) {
841 localHLS.startConverting();
845 if (cameraConfig.getGifPreroll() > 0) {
846 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
847 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
848 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
849 + cameraConfig.getGifOutOptions(),
850 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
851 cameraConfig.getPassword());
853 if (!inputOptions.isEmpty()) {
854 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
856 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
858 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
859 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
860 cameraConfig.getUser(), cameraConfig.getPassword());
862 if (cameraConfig.getGifPreroll() > 0) {
865 Ffmpeg localGIF = ffmpegGIF;
866 if (localGIF != null) {
867 localGIF.startConverting();
868 if (gifHistory.isEmpty()) {
869 gifHistory = gifFilename;
870 } else if (!"ipcamera".equals(gifFilename)) {
871 gifHistory = gifFilename + "," + gifHistory;
872 if (gifHistoryLength > 49) {
873 int endIndex = gifHistory.lastIndexOf(",");
874 gifHistory = gifHistory.substring(0, endIndex);
877 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
881 if (!inputOptions.isEmpty()) {
882 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
884 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
886 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
887 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
888 cameraConfig.getUser(), cameraConfig.getPassword());
889 ffmpegRecord.startConverting();
890 if (mp4History.isEmpty()) {
891 mp4History = mp4Filename;
892 } else if (!"ipcamera".equals(mp4Filename)) {
893 mp4History = mp4Filename + "," + mp4History;
894 if (mp4HistoryLength > 49) {
895 int endIndex = mp4History.lastIndexOf(",");
896 mp4History = mp4History.substring(0, endIndex);
899 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
902 Ffmpeg localAlarms = ffmpegRtspHelper;
903 if (localAlarms != null) {
904 localAlarms.stopConverting();
905 if (!ffmpegAudioAlarmEnabled && !ffmpegMotionAlarmEnabled) {
909 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
910 String filterOptions = "";
911 if (!ffmpegAudioAlarmEnabled) {
912 filterOptions = "-an";
914 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
916 if (!ffmpegMotionAlarmEnabled && !ffmpegSnapshotGeneration) {
917 filterOptions = filterOptions.concat(" -vn");
918 } else if (ffmpegMotionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
919 String usersMotionOptions = cameraConfig.getMotionOptions();
920 if (usersMotionOptions.startsWith("-")) {
921 // Need to put the users custom options first in the chain before the motion is detected
922 filterOptions += " " + usersMotionOptions + ",select='gte(scene,"
923 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
925 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
926 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
928 } else if (ffmpegMotionAlarmEnabled) {
929 filterOptions = filterOptions.concat(" -vf select='gte(scene,"
930 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print");
932 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
933 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
934 ffmpegRtspHelper.startConverting();
937 if (ffmpegMjpeg == null) {
938 if (inputOptions.isEmpty()) {
939 inputOptions = "-hide_banner";
941 inputOptions += " -hide_banner";
943 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
944 cameraConfig.getMjpegOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
945 + getThing().getUID().getId() + "/ipcamera.jpg",
946 cameraConfig.getUser(), cameraConfig.getPassword());
948 Ffmpeg localMjpeg = ffmpegMjpeg;
949 if (localMjpeg != null) {
950 localMjpeg.startConverting();
954 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
955 if (ffmpegSnapshot == null) {
956 if (inputOptions.isEmpty()) {
958 inputOptions = "-threads 1 -skip_frame nokey -hide_banner";
960 inputOptions += " -threads 1 -skip_frame nokey -hide_banner";
962 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
963 cameraConfig.getSnapshotOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
964 + getThing().getUID().getId() + "/snapshot.jpg",
965 cameraConfig.getUser(), cameraConfig.getPassword());
967 Ffmpeg localSnaps = ffmpegSnapshot;
968 if (localSnaps != null) {
969 localSnaps.startConverting();
975 public void noMotionDetected(String thisAlarmsChannel) {
976 setChannelState(thisAlarmsChannel, OnOffType.OFF);
977 firstMotionAlarm = false;
978 motionAlarmUpdateSnapshot = false;
979 motionDetected = false;
980 if (streamingAutoFps) {
981 stopSnapshotPolling();
982 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
983 stopSnapshotPolling();
988 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
989 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
990 * tampering with the camera.
992 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
993 updateState(thisAlarmsChannel, state);
996 public void motionDetected(String thisAlarmsChannel) {
997 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
998 updateState(thisAlarmsChannel, OnOffType.ON);
999 motionDetected = true;
1000 if (streamingAutoFps) {
1001 startSnapshotPolling();
1003 if (cameraConfig.getUpdateImageWhen().contains("2")) {
1004 if (!firstMotionAlarm) {
1005 if (!snapshotUri.isEmpty()) {
1008 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1010 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1011 if (!snapshotPolling) {
1012 startSnapshotPolling();
1014 firstMotionAlarm = true;
1015 motionAlarmUpdateSnapshot = true;
1019 public void audioDetected() {
1020 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1021 if (cameraConfig.getUpdateImageWhen().contains("3")) {
1022 if (!firstAudioAlarm) {
1023 if (!snapshotUri.isEmpty()) {
1026 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1028 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1029 firstAudioAlarm = true;
1030 audioAlarmUpdateSnapshot = true;
1034 public void noAudioDetected() {
1035 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1036 firstAudioAlarm = false;
1037 audioAlarmUpdateSnapshot = false;
1040 public void recordMp4(String filename, int seconds) {
1041 mp4Filename = filename;
1042 mp4RecordTime = seconds;
1043 setupFfmpegFormat(FFmpegFormat.RECORD);
1044 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1047 public void recordGif(String filename, int seconds) {
1048 gifFilename = filename;
1049 gifRecordTime = seconds;
1050 if (cameraConfig.getGifPreroll() > 0) {
1051 snapCount = seconds;
1053 setupFfmpegFormat(FFmpegFormat.GIF);
1055 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1058 private void getReolinkToken() {
1059 sendHttpPOST("/api.cgi?cmd=Login",
1060 "[{\"cmd\":\"Login\", \"param\":{ \"User\":{ \"Version\": \"0\", \"userName\":\""
1061 + cameraConfig.getUser() + "\", \"password\":\"" + cameraConfig.getPassword() + "\"}}}]");
1064 public String returnValueFromString(String rawString, String searchedString) {
1066 int index = rawString.indexOf(searchedString);
1067 if (index != -1) // -1 means "not found"
1069 result = rawString.substring(index + searchedString.length(), rawString.length());
1070 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1072 return result; // Did not find a carriage return.
1074 return result.substring(0, index);
1077 return ""; // Did not find the String we were searching for
1080 private void sendPTZRequest() {
1081 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1085 public void channelLinked(ChannelUID channelUID) {
1086 switch (channelUID.getId()) {
1087 case CHANNEL_MJPEG_URL:
1088 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1089 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
1091 case CHANNEL_HLS_URL:
1092 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1093 + getThing().getUID().getId() + "/ipcamera.m3u8"));
1095 case CHANNEL_IMAGE_URL:
1096 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1097 + getThing().getUID().getId() + "/ipcamera.jpg"));
1102 public void removeChannels(List<org.openhab.core.thing.Channel> removeChannels) {
1103 if (!removeChannels.isEmpty()) {
1104 ThingBuilder thingBuilder = editThing();
1105 thingBuilder.withoutChannels(removeChannels);
1106 updateThing(thingBuilder.build());
1111 public void handleCommand(ChannelUID channelUID, Command command) {
1112 if (command instanceof RefreshType) {
1113 switch (channelUID.getId()) {
1115 if (onvifCamera.supportsPTZ()) {
1116 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1120 if (onvifCamera.supportsPTZ()) {
1121 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1125 if (onvifCamera.supportsPTZ()) {
1126 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1129 case CHANNEL_GOTO_PRESET:
1130 if (onvifCamera.supportsPTZ()) {
1131 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1135 } // caution "REFRESH" can still progress to brand Handlers below the else.
1137 switch (channelUID.getId()) {
1138 case CHANNEL_MP4_HISTORY_LENGTH:
1139 if (DecimalType.ZERO.equals(command)) {
1140 mp4HistoryLength = 0;
1142 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1145 case CHANNEL_GIF_HISTORY_LENGTH:
1146 if (DecimalType.ZERO.equals(command)) {
1147 gifHistoryLength = 0;
1149 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1152 case CHANNEL_FFMPEG_MOTION_CONTROL:
1153 if (OnOffType.ON.equals(command)) {
1154 ffmpegMotionAlarmEnabled = true;
1155 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1156 ffmpegMotionAlarmEnabled = false;
1157 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1158 } else if (command instanceof PercentType percentCommand) {
1159 ffmpegMotionAlarmEnabled = true;
1160 motionThreshold = percentCommand.toBigDecimal();
1162 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1164 case CHANNEL_START_STREAM:
1166 if (OnOffType.ON.equals(command)) {
1167 localHLS = ffmpegHLS;
1168 if (localHLS == null) {
1169 setupFfmpegFormat(FFmpegFormat.HLS);
1170 localHLS = ffmpegHLS;
1172 if (localHLS != null) {
1173 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1174 localHLS.startConverting();
1177 localHLS = ffmpegHLS;
1178 if (localHLS != null) {
1179 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1180 localHLS.setKeepAlive(1);
1184 case CHANNEL_EXTERNAL_MOTION:
1185 if (OnOffType.ON.equals(command)) {
1186 motionDetected(CHANNEL_EXTERNAL_MOTION);
1188 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1191 case CHANNEL_GOTO_PRESET:
1192 if (onvifCamera.supportsPTZ()) {
1193 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1196 case CHANNEL_POLL_IMAGE:
1197 if (OnOffType.ON.equals(command)) {
1198 if (snapshotUri.isEmpty()) {
1199 ffmpegSnapshotGeneration = true;
1200 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1201 updateImageChannel = false;
1203 updateImageChannel = true;
1204 updateSnapshot();// Allows this to change Image FPS on demand
1207 Ffmpeg localSnaps = ffmpegSnapshot;
1208 if (localSnaps != null) {
1209 localSnaps.stopConverting();
1210 ffmpegSnapshotGeneration = false;
1212 updateImageChannel = false;
1216 if (onvifCamera.supportsPTZ()) {
1217 if (command instanceof IncreaseDecreaseType) {
1218 if (command == IncreaseDecreaseType.INCREASE) {
1219 if (cameraConfig.getPtzContinuous()) {
1220 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1222 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1225 if (cameraConfig.getPtzContinuous()) {
1226 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1228 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1232 } else if (OnOffType.OFF.equals(command)) {
1233 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1236 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1237 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1241 if (onvifCamera.supportsPTZ()) {
1242 if (command instanceof IncreaseDecreaseType) {
1243 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1244 if (cameraConfig.getPtzContinuous()) {
1245 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1247 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1250 if (cameraConfig.getPtzContinuous()) {
1251 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1253 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1257 } else if (OnOffType.OFF.equals(command)) {
1258 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1261 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1262 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1266 if (onvifCamera.supportsPTZ()) {
1267 if (command instanceof IncreaseDecreaseType) {
1268 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1269 if (cameraConfig.getPtzContinuous()) {
1270 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1272 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1275 if (cameraConfig.getPtzContinuous()) {
1276 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1278 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1282 } else if (OnOffType.OFF.equals(command)) {
1283 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1286 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1287 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1292 // commands and refresh now get passed to brand handlers
1293 switch (thing.getThingTypeUID().getId()) {
1295 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1296 amcrestHandler.handleCommand(channelUID, command);
1297 if (lowPriorityRequests.isEmpty()) {
1298 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1302 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1303 dahuaHandler.handleCommand(channelUID, command);
1304 if (lowPriorityRequests.isEmpty()) {
1305 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1308 case DOORBIRD_THING:
1309 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1310 doorBirdHandler.handleCommand(channelUID, command);
1311 if (lowPriorityRequests.isEmpty()) {
1312 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1315 case HIKVISION_THING:
1316 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1317 hikvisionHandler.handleCommand(channelUID, command);
1320 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1321 cameraConfig.getPassword());
1322 foscamHandler.handleCommand(channelUID, command);
1323 if (lowPriorityRequests.isEmpty()) {
1324 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1328 InstarHandler instarHandler = new InstarHandler(getHandle());
1329 instarHandler.handleCommand(channelUID, command);
1330 if (lowPriorityRequests.isEmpty()) {
1331 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1335 ReolinkHandler reolinkHandler = new ReolinkHandler(getHandle());
1336 reolinkHandler.handleCommand(channelUID, command);
1339 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1340 defaultHandler.handleCommand(channelUID, command);
1341 if (lowPriorityRequests.isEmpty()) {
1342 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1348 public void setChannelState(String channelToUpdate, State valueOf) {
1349 updateState(channelToUpdate, valueOf);
1352 private void bringCameraOnline() {
1354 updateStatus(ThingStatus.ONLINE);
1355 groupTracker.listOfOnlineCameraHandlers.add(this);
1356 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1357 Future<?> localFuture = cameraConnectionJob;
1358 if (localFuture != null) {
1359 localFuture.cancel(false);
1360 cameraConnectionJob = null;
1362 if (!snapshotUri.isEmpty()) {
1363 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1364 snapshotPolling = true;
1365 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000,
1366 cameraConfig.getPollTime(), TimeUnit.MILLISECONDS);
1370 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1372 // auto restart mjpeg stream now camera is back online.
1373 CameraServlet localServlet = servlet;
1374 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1375 openCamerasStream();
1378 if (!rtspUri.isEmpty()) {
1379 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1381 if (updateImageChannel) {
1382 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1384 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1386 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1387 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1388 handle.cameraOnline(getThing().getUID().getId());
1393 void snapshotIsFfmpeg() {
1394 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1396 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1397 bringCameraOnline();
1398 if (!rtspUri.isEmpty()) {
1399 updateImageChannel = false;
1400 ffmpegSnapshotGeneration = true;
1401 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1402 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1404 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1408 void pollingCameraConnection() {
1410 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)
1411 || thing.getThingTypeUID().getId().equals(DOORBIRD_THING)) {
1412 if (rtspUri.isEmpty()) {
1413 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1415 if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1418 ffmpegSnapshotGeneration = false;
1423 if (cameraConfig.getOnvifPort() > 0 && !onvifCamera.isConnected()) {
1424 if (onvifCamera.isConnectError()) {
1425 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Camera is not reachable");
1426 } else if (onvifCamera.isRefusedError()) {
1427 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1428 "Camera refused connection on ONVIF ports.");
1430 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP: {}:{}", cameraConfig.getIp(),
1431 cameraConfig.getOnvifPort());
1432 onvifCamera.connect(supportsOnvifEvents());
1435 if ("ffmpeg".equals(snapshotUri)) {
1437 } else if (!snapshotUri.isEmpty()) {
1438 ffmpegSnapshotGeneration = false;
1440 } else if (!rtspUri.isEmpty()) {
1443 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1444 "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.");
1448 public void cameraConfigError(String reason) {
1449 // won't try to reconnect again due to a config error being the cause.
1450 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1454 public void cameraCommunicationError(String reason) {
1455 // will try to reconnect again as camera may be rebooting.
1456 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1457 if (isOnline) { // if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1458 resetAndRetryConnecting();
1462 private boolean streamIsStopped(String url) {
1463 ChannelTracking channelTracking = channelTrackingMap.get(url);
1464 if (channelTracking != null) {
1465 if (channelTracking.getChannel().isActive()) {
1466 return false; // stream is running.
1469 return true; // Stream stopped or never started.
1472 void snapshotRunnable() {
1473 // Snapshot should be first to keep consistent time between shots
1475 if (snapCount > 0) {
1476 if (--snapCount == 0) {
1477 setupFfmpegFormat(FFmpegFormat.GIF);
1482 private void takeSnapshot() {
1483 sendHttpGET(snapshotUri);
1486 private void updateSnapshot() {
1487 lastSnapshotRequest = Instant.now();
1488 mainEventLoopGroup.schedule(this::takeSnapshot, 0, TimeUnit.MILLISECONDS);
1491 public byte[] getSnapshot() {
1493 // Single gray pixel JPG to keep streams open when the camera goes offline so they dont stop.
1494 return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
1495 0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43,
1496 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
1497 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09,
1498 0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d,
1499 0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10,
1500 0x10, (byte) 0xff, (byte) 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
1501 (byte) 0xff, (byte) 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, (byte) 0xff, (byte) 0xda, 0x00, 0x08,
1502 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, (byte) 0xd2, (byte) 0xcf, 0x20, (byte) 0xff, (byte) 0xd9 };
1504 // Most cameras will return a 503 busy error if snapshot is faster than 1 second
1505 long lastUpdatedMs = Duration.between(lastSnapshotRequest, Instant.now()).toMillis();
1506 if (!snapshotPolling && !ffmpegSnapshotGeneration && lastUpdatedMs >= cameraConfig.getPollTime()) {
1509 lockCurrentSnapshot.lock();
1511 return currentSnapshot;
1513 lockCurrentSnapshot.unlock();
1517 public void stopSnapshotPolling() {
1518 Future<?> localFuture;
1519 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1520 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1521 snapshotPolling = false;
1522 localFuture = snapshotJob;
1523 if (localFuture != null) {
1524 localFuture.cancel(true);
1526 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1527 snapshotPolling = false;
1528 localFuture = snapshotJob;
1529 if (localFuture != null) {
1530 localFuture.cancel(true);
1535 public void startSnapshotPolling() {
1536 if (snapshotPolling || ffmpegSnapshotGeneration) {
1537 return; // Already polling or creating with FFmpeg from RTSP
1539 if (streamingSnapshotMjpeg || streamingAutoFps || cameraConfig.getUpdateImageWhen().contains("4")) {
1540 snapshotPolling = true;
1541 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 0, cameraConfig.getPollTime(),
1542 TimeUnit.MILLISECONDS);
1547 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep alarm
1548 * streams open and more.
1551 void pollCameraRunnable() {
1552 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1553 if (!lowPriorityRequests.isEmpty()) {
1554 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1555 lowPriorityCounter = 0;
1557 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1559 // what needs to be done every poll//
1560 switch (thing.getThingTypeUID().getId()) {
1562 if (!snapshotPolling) {
1563 checkCameraConnection();
1567 if (!snapshotPolling) {
1568 checkCameraConnection();
1572 if (!snapshotPolling) {
1573 checkCameraConnection();
1575 noMotionDetected(CHANNEL_MOTION_ALARM);
1576 noMotionDetected(CHANNEL_PIR_ALARM);
1577 noMotionDetected(CHANNEL_HUMAN_ALARM);
1578 noMotionDetected(CHANNEL_CAR_ALARM);
1579 noMotionDetected(CHANNEL_ANIMAL_ALARM);
1582 case HIKVISION_THING:
1583 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1584 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1585 cameraConfig.getIp());
1586 sendHttpGET("/ISAPI/Event/notification/alertStream");
1590 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1591 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1594 if (cameraConfig.getNvrChannel() > 0) {
1595 sendHttpGET("/api.cgi?cmd=GetAiState&channel=" + cameraConfig.getNvrChannel() + "&user="
1596 + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword());
1597 sendHttpGET("/api.cgi?cmd=GetMdState&channel=" + cameraConfig.getNvrChannel() + "&user="
1598 + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword());
1600 if (!snapshotPolling) {
1601 checkCameraConnection();
1603 if (!onvifCamera.isConnected()) {
1604 onvifCamera.connect(true);
1609 if (!snapshotPolling) {
1610 checkCameraConnection();
1612 // Check for alarms, channel for NVRs appears not to work at filtering.
1613 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1614 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1615 cameraConfig.getIp());
1616 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1619 case DOORBIRD_THING:
1620 if (!snapshotPolling) {
1621 checkCameraConnection();
1623 // Check for alarms, channel for NVRs appears not to work at filtering.
1624 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1625 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1626 cameraConfig.getIp());
1627 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1631 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1632 + cameraConfig.getPassword());
1635 Ffmpeg localFfmpeg = ffmpegHLS;
1636 if (localFfmpeg != null) {
1637 localFfmpeg.checkKeepAlive();
1639 if (ffmpegMotionAlarmEnabled || ffmpegAudioAlarmEnabled) {
1640 localFfmpeg = ffmpegRtspHelper;
1641 if (localFfmpeg == null || !localFfmpeg.isAlive()) {
1642 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1645 // check if the thread has frozen due to camera doing a soft reboot
1646 localFfmpeg = ffmpegMjpeg;
1647 if (localFfmpeg != null && !localFfmpeg.isAlive()) {
1648 logger.debug("MJPEG was not being produced by FFmpeg when it should have been, restarting FFmpeg.");
1649 setupFfmpegFormat(FFmpegFormat.MJPEG);
1651 if (openChannels.size() > 10) {
1652 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1658 public void initialize() {
1659 cameraConfig = getConfigAs(CameraConfig.class);
1660 threadPool = Executors.newScheduledThreadPool(2);
1661 mainEventLoopGroup = new NioEventLoopGroup(3);
1662 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1663 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1664 rtspUri = cameraConfig.getFfmpegInput();
1665 if (cameraConfig.getFfmpegOutput().isEmpty()) {
1667 .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1669 // Known cameras will connect quicker if we skip ONVIF questions.
1670 switch (thing.getThingTypeUID().getId()) {
1673 if (mjpegUri.isEmpty()) {
1674 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1676 if (snapshotUri.isEmpty()) {
1677 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1680 case DOORBIRD_THING:
1681 if (mjpegUri.isEmpty()) {
1682 mjpegUri = "/bha-api/video.cgi";
1684 if (snapshotUri.isEmpty()) {
1685 snapshotUri = "/bha-api/image.cgi";
1689 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1690 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1691 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1692 if (mjpegUri.isEmpty()) {
1693 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1694 + cameraConfig.getPassword();
1696 if (snapshotUri.isEmpty()) {
1697 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1698 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1701 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1702 if (mjpegUri.isEmpty()) {
1703 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1705 if (snapshotUri.isEmpty()) {
1706 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1708 if (lowPriorityRequests.isEmpty()) {
1709 lowPriorityRequests.add("/ISAPI/System/IO/capabilities");
1713 if (snapshotUri.isEmpty()) {
1714 snapshotUri = "/tmpfs/snap.jpg";
1716 if (mjpegUri.isEmpty()) {
1717 mjpegUri = "/mjpegstream.cgi?-chn=12";
1719 // Newer Instar cameras use this to setup the Alarm Server, plus it is used to work out which API is
1720 // implemented based on the response to these two requests.
1722 "/param.cgi?cmd=setasaction&-server=1&enable=1&-interval=1&cmd=setasattr&-as_index=1&-as_server="
1723 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1724 + getThing().getUID().getId()
1725 + "/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");
1726 // Older Instar cameras use this to setup the Alarm Server
1728 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
1729 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1730 + getThing().getUID().getId()
1731 + "/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");
1734 if (cameraConfig.useToken) {
1735 authenticationJob = threadPool.scheduleWithFixedDelay(this::getReolinkToken, 0, 45,
1738 reolinkAuth = "&user=" + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword();
1740 if (snapshotUri.isEmpty()) {
1741 if (cameraConfig.getNvrChannel() < 1) {
1742 snapshotUri = "/cgi-bin/api.cgi?cmd=Snap&channel=0&rs=openHAB" + reolinkAuth;
1744 snapshotUri = "/cgi-bin/api.cgi?cmd=Snap&channel=" + (cameraConfig.getNvrChannel() - 1)
1745 + "&rs=openHAB" + reolinkAuth;
1748 if (rtspUri.isEmpty()) {
1749 if (cameraConfig.getNvrChannel() < 1) {
1750 rtspUri = "rtsp://" + cameraConfig.getIp() + ":554/h264Preview_01_main";
1752 rtspUri = "rtsp://" + cameraConfig.getIp() + ":554/h264Preview_0" + cameraConfig.getNvrChannel()
1758 // for poll times 9 seconds and above don't display a warning about the Image channel.
1759 if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1761 "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.");
1763 // ONVIF and Instar event handling need the server started before connecting.
1764 startStreamServer();
1768 private void tryConnecting() {
1770 int normalDelay = 12; // doesn't make sense to have faster retry than CONNECT_TIMEOUT, which is 10 seconds, if
1772 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)
1773 && !thing.getThingTypeUID().getId().equals(DOORBIRD_THING) && cameraConfig.getOnvifPort() > 0) {
1774 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1775 cameraConfig.getUser(), cameraConfig.getPassword());
1776 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1777 // Only use ONVIF events if it is not an API camera.
1778 onvifCamera.connect(supportsOnvifEvents());
1780 if (supportsOnvifEvents()) {
1781 // it takes some time to try to retrieve the ONVIF snapshot and stream URLs and update internal members
1782 // on first connect; if connection lost, doesn't make sense to poll to often
1787 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, firstDelay, normalDelay,
1791 private boolean supportsOnvifEvents() {
1792 switch (thing.getThingTypeUID().getId()) {
1796 if (cameraConfig.getNvrChannel() < 1) {
1803 private void keepMjpegRunning() {
1804 CameraServlet localServlet = servlet;
1805 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1806 if (!mjpegUri.isEmpty() && !"ffmpeg".equals(mjpegUri)) {
1807 localServlet.openStreams.queueFrame(("--" + localServlet.openStreams.boundary + "\r\n\r\n").getBytes());
1809 localServlet.openStreams.queueFrame(getSnapshot());
1813 // What the camera needs to re-connect if the initialize() is not called.
1814 private void resetAndRetryConnecting() {
1819 private void offline() {
1821 snapshotPolling = false;
1822 Future<?> localFuture = pollCameraJob;
1823 if (localFuture != null) {
1824 localFuture.cancel(true);
1825 pollCameraJob = null;
1827 localFuture = authenticationJob;
1828 if (localFuture != null) {
1829 localFuture.cancel(true);
1830 authenticationJob = null;
1832 localFuture = snapshotJob;
1833 if (localFuture != null) {
1834 localFuture.cancel(true);
1837 localFuture = cameraConnectionJob;
1838 if (localFuture != null) {
1839 localFuture.cancel(true);
1840 cameraConnectionJob = null;
1842 Ffmpeg localFfmpeg = ffmpegHLS;
1843 if (localFfmpeg != null) {
1844 localFfmpeg.stopConverting();
1847 localFfmpeg = ffmpegRecord;
1848 if (localFfmpeg != null) {
1849 localFfmpeg.stopConverting();
1850 ffmpegRecord = null;
1852 localFfmpeg = ffmpegGIF;
1853 if (localFfmpeg != null) {
1854 localFfmpeg.stopConverting();
1857 localFfmpeg = ffmpegRtspHelper;
1858 if (localFfmpeg != null) {
1859 localFfmpeg.stopConverting();
1860 ffmpegRtspHelper = null;
1862 localFfmpeg = ffmpegMjpeg;
1863 if (localFfmpeg != null) {
1864 localFfmpeg.stopConverting();
1867 localFfmpeg = ffmpegSnapshot;
1868 if (localFfmpeg != null) {
1869 localFfmpeg.stopConverting();
1870 ffmpegSnapshot = null;
1872 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) { // generic cameras do not have ONVIF support
1873 onvifCamera.disconnect();
1875 openChannels.close();
1879 public void dispose() {
1881 CameraServlet localServlet = servlet;
1882 if (localServlet != null) {
1883 localServlet.dispose();
1886 threadPool.shutdown();
1887 // inform all group handlers that this camera has gone offline
1888 groupTracker.listOfOnlineCameraHandlers.remove(this);
1889 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1890 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1891 handle.cameraOffline(this);
1893 basicAuth = ""; // clear out stored Password hash
1894 useDigestAuth = false;
1895 mainEventLoopGroup.shutdownGracefully();
1896 mainBootstrap = null;
1897 channelTrackingMap.clear();
1900 public String getWhiteList() {
1901 return cameraConfig.getIpWhitelist();
1905 public Collection<Class<? extends ThingHandlerService>> getServices() {
1906 return Set.of(IpCameraActions.class);