2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.ipcamera.internal.handler;
15 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
18 import java.io.FileNotFoundException;
19 import java.io.FileOutputStream;
20 import java.io.IOException;
21 import java.io.OutputStream;
22 import java.math.BigDecimal;
23 import java.net.InetSocketAddress;
24 import java.net.MalformedURLException;
26 import java.nio.charset.StandardCharsets;
27 import java.time.Duration;
28 import java.time.Instant;
29 import java.util.ArrayList;
30 import java.util.Collection;
31 import java.util.LinkedList;
32 import java.util.List;
35 import java.util.concurrent.ConcurrentHashMap;
36 import java.util.concurrent.Executors;
37 import java.util.concurrent.Future;
38 import java.util.concurrent.ScheduledExecutorService;
39 import java.util.concurrent.ScheduledFuture;
40 import java.util.concurrent.TimeUnit;
41 import java.util.concurrent.locks.ReentrantLock;
43 import org.eclipse.jdt.annotation.NonNullByDefault;
44 import org.eclipse.jdt.annotation.Nullable;
45 import org.openhab.binding.ipcamera.internal.AmcrestHandler;
46 import org.openhab.binding.ipcamera.internal.CameraConfig;
47 import org.openhab.binding.ipcamera.internal.ChannelTracking;
48 import org.openhab.binding.ipcamera.internal.DahuaHandler;
49 import org.openhab.binding.ipcamera.internal.DoorBirdHandler;
50 import org.openhab.binding.ipcamera.internal.Ffmpeg;
51 import org.openhab.binding.ipcamera.internal.FoscamHandler;
52 import org.openhab.binding.ipcamera.internal.GroupTracker;
53 import org.openhab.binding.ipcamera.internal.Helper;
54 import org.openhab.binding.ipcamera.internal.HikvisionHandler;
55 import org.openhab.binding.ipcamera.internal.HttpOnlyHandler;
56 import org.openhab.binding.ipcamera.internal.InstarHandler;
57 import org.openhab.binding.ipcamera.internal.IpCameraActions;
58 import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
59 import org.openhab.binding.ipcamera.internal.IpCameraDynamicStateDescriptionProvider;
60 import org.openhab.binding.ipcamera.internal.MyNettyAuthHandler;
61 import org.openhab.binding.ipcamera.internal.ReolinkHandler;
62 import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection;
63 import org.openhab.binding.ipcamera.internal.servlet.CameraServlet;
64 import org.openhab.core.OpenHAB;
65 import org.openhab.core.library.types.DecimalType;
66 import org.openhab.core.library.types.IncreaseDecreaseType;
67 import org.openhab.core.library.types.OnOffType;
68 import org.openhab.core.library.types.PercentType;
69 import org.openhab.core.library.types.RawType;
70 import org.openhab.core.library.types.StringType;
71 import org.openhab.core.thing.ChannelUID;
72 import org.openhab.core.thing.Thing;
73 import org.openhab.core.thing.ThingStatus;
74 import org.openhab.core.thing.ThingStatusDetail;
75 import org.openhab.core.thing.binding.BaseThingHandler;
76 import org.openhab.core.thing.binding.ThingHandlerService;
77 import org.openhab.core.thing.binding.builder.ThingBuilder;
78 import org.openhab.core.types.Command;
79 import org.openhab.core.types.RefreshType;
80 import org.openhab.core.types.State;
81 import org.osgi.framework.FrameworkUtil;
82 import org.osgi.service.http.HttpService;
83 import org.slf4j.Logger;
84 import org.slf4j.LoggerFactory;
86 import io.netty.bootstrap.Bootstrap;
87 import io.netty.buffer.ByteBuf;
88 import io.netty.buffer.Unpooled;
89 import io.netty.channel.Channel;
90 import io.netty.channel.ChannelDuplexHandler;
91 import io.netty.channel.ChannelFuture;
92 import io.netty.channel.ChannelFutureListener;
93 import io.netty.channel.ChannelHandlerContext;
94 import io.netty.channel.ChannelInitializer;
95 import io.netty.channel.ChannelOption;
96 import io.netty.channel.EventLoopGroup;
97 import io.netty.channel.group.ChannelGroup;
98 import io.netty.channel.group.DefaultChannelGroup;
99 import io.netty.channel.nio.NioEventLoopGroup;
100 import io.netty.channel.socket.SocketChannel;
101 import io.netty.channel.socket.nio.NioSocketChannel;
102 import io.netty.handler.codec.base64.Base64;
103 import io.netty.handler.codec.http.DefaultFullHttpRequest;
104 import io.netty.handler.codec.http.FullHttpRequest;
105 import io.netty.handler.codec.http.HttpClientCodec;
106 import io.netty.handler.codec.http.HttpContent;
107 import io.netty.handler.codec.http.HttpHeaderValues;
108 import io.netty.handler.codec.http.HttpMessage;
109 import io.netty.handler.codec.http.HttpMethod;
110 import io.netty.handler.codec.http.HttpResponse;
111 import io.netty.handler.codec.http.HttpVersion;
112 import io.netty.handler.codec.http.LastHttpContent;
113 import io.netty.handler.timeout.IdleState;
114 import io.netty.handler.timeout.IdleStateEvent;
115 import io.netty.handler.timeout.IdleStateHandler;
116 import io.netty.util.CharsetUtil;
117 import io.netty.util.ReferenceCountUtil;
118 import io.netty.util.concurrent.GlobalEventExecutor;
121 * The {@link IpCameraHandler} is responsible for handling commands, which are
122 * sent to one of the channels.
124 * @author Matthew Skinner - Initial contribution
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<byte[]>();
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 boolean useBasicAuth = false;
178 public boolean useDigestAuth = false;
179 public boolean newInstarApi = false;
180 public String snapshotUri = "";
181 public String mjpegUri = "";
182 private byte[] currentSnapshot = new byte[] { (byte) 0x00 };
183 public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
184 public String rtspUri = "";
185 public boolean audioAlarmUpdateSnapshot = false;
186 private boolean motionAlarmUpdateSnapshot = false;
187 private boolean isOnline = false; // Used so only 1 error is logged when a network issue occurs.
188 private boolean firstAudioAlarm = false;
189 private boolean firstMotionAlarm = false;
190 public BigDecimal motionThreshold = BigDecimal.ZERO;
191 public int audioThreshold = 35;
192 public boolean streamingSnapshotMjpeg = false;
193 public boolean ffmpegMotionAlarmEnabled = false;
194 public boolean ffmpegAudioAlarmEnabled = false;
195 public boolean ffmpegSnapshotGeneration = false;
196 public boolean snapshotPolling = false;
197 public OnvifConnection onvifCamera = new OnvifConnection(this, "", "", "");
199 // These methods handle the response from all camera brands, nothing specific to 1 brand.
200 private class CommonCameraHandler extends ChannelDuplexHandler {
201 private int bytesToRecieve = 0;
202 private int bytesAlreadyRecieved = 0;
203 private byte[] incomingJpeg = new byte[0];
204 private String incomingMessage = "";
205 private String contentType = "empty";
206 private String boundary = "";
207 private Object reply = new Object();
208 private String requestUrl = "";
209 private boolean isChunked = false;
211 public void setURL(String url) {
216 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
217 if (msg == null || ctx == null) {
221 if (msg instanceof HttpResponse response) {
222 if (response.status().code() == 200) {
223 if (!response.headers().isEmpty()) {
224 for (String name : response.headers().names()) {
225 // Some cameras use first letter uppercase and others dont.
226 switch (name.toLowerCase()) { // Possible localization issues doing this
228 contentType = response.headers().getAsString(name);
230 case "content-length":
231 bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
233 case "transfer-encoding":
234 if (response.headers().getAsString(name).contains("chunked")) {
240 if (contentType.contains("multipart")) {
241 boundary = Helper.searchString(contentType, "boundary=");
242 if (mjpegUri.equals(requestUrl)) {
243 if (msg instanceof HttpMessage) {
244 // very start of stream only
245 mjpegContentType = contentType;
246 CameraServlet localServlet = servlet;
247 if (localServlet != null) {
248 logger.debug("Setting Content-Type to:{}", contentType);
249 localServlet.openStreams.updateContentType(contentType, boundary);
253 } else if (contentType.contains("image/jp")) {
254 if (bytesToRecieve == 0) {
255 bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
256 logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
258 incomingJpeg = new byte[bytesToRecieve];
262 // Non 200 OK replies are logged and handled in pipeline by MyNettyAuthHandler.java
266 if (msg instanceof HttpContent content) {
267 if (mjpegUri.equals(requestUrl) && !(content instanceof LastHttpContent)) {
268 // multiple MJPEG stream packets come back as this.
269 byte[] chunkedFrame = new byte[content.content().readableBytes()];
270 content.content().getBytes(content.content().readerIndex(), chunkedFrame);
271 CameraServlet localServlet = servlet;
272 if (localServlet != null) {
273 localServlet.openStreams.queueFrame(chunkedFrame);
276 // Found some cameras use Content-Type: image/jpg instead of image/jpeg
277 if (contentType.contains("image/jp")) {
278 for (int i = 0; i < content.content().capacity(); i++) {
279 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
281 if (content instanceof LastHttpContent) {
282 processSnapshot(incomingJpeg);
285 } else { // incomingMessage that is not an IMAGE
286 if (incomingMessage.isEmpty()) {
287 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
289 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
291 bytesAlreadyRecieved = incomingMessage.length();
292 if (content instanceof LastHttpContent) {
293 // If it is not an image send it on to the next handler//
294 if (bytesAlreadyRecieved != 0) {
295 reply = incomingMessage;
296 super.channelRead(ctx, reply);
299 // Alarm Streams never have a LastHttpContent as they always stay open//
300 else if (contentType.contains("multipart")) {
301 int beginIndex, endIndex;
302 if (bytesToRecieve == 0) {
303 beginIndex = incomingMessage.indexOf("Content-Length:");
304 if (beginIndex != -1) {
305 endIndex = incomingMessage.indexOf("\r\n", beginIndex);
306 if (endIndex != -1) {
307 bytesToRecieve = Integer.parseInt(
308 incomingMessage.substring(beginIndex + 15, endIndex).strip());
312 // --boundary and headers are not included in the Content-Length value
313 if (bytesAlreadyRecieved > bytesToRecieve) {
314 // Check if message has a second --boundary
315 endIndex = incomingMessage.indexOf("--" + boundary, bytesToRecieve);
316 if (endIndex == -1) {
317 reply = incomingMessage;
318 incomingMessage = "";
320 bytesAlreadyRecieved = 0;
322 reply = incomingMessage.substring(0, endIndex);
323 incomingMessage = incomingMessage.substring(endIndex, incomingMessage.length());
324 bytesToRecieve = 0;// Triggers search next time for Content-Length:
325 bytesAlreadyRecieved = incomingMessage.length() - endIndex;
327 super.channelRead(ctx, reply);
330 // Foscam needs this as will other cameras with chunks//
331 if (isChunked && bytesAlreadyRecieved != 0) {
332 reply = incomingMessage;
336 } else { // msg is not HttpContent
337 // Foscam cameras need this
338 if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
339 reply = incomingMessage;
340 logger.trace("Packet back from camera is {}", incomingMessage);
341 super.channelRead(ctx, reply);
345 ReferenceCountUtil.release(msg);
350 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
351 if (cause == null || ctx == null) {
354 if (cause instanceof ArrayIndexOutOfBoundsException) {
355 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
358 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
365 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
369 if (evt instanceof IdleStateEvent e) {
370 // If camera does not use the channel for X amount of time it will close.
371 if (e.state() == IdleState.READER_IDLE) {
372 String urlToKeepOpen = "";
373 switch (thing.getThingTypeUID().getId()) {
375 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
378 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
381 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
382 if (channelTracking != null) {
383 if (channelTracking.getChannel().equals(ctx.channel())) {
384 return; // don't auto close this as it is for the alarms.
387 logger.debug("Closing an idle channel for camera:{}", cameraConfig.getIp());
394 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
395 IpCameraDynamicStateDescriptionProvider stateDescriptionProvider, HttpService httpService) {
397 this.stateDescriptionProvider = stateDescriptionProvider;
398 if (ipAddress != null) {
401 hostIp = Helper.getLocalIpAddress();
403 this.groupTracker = groupTracker;
404 this.httpService = httpService;
407 private IpCameraHandler getHandle() {
411 // false clears the stored user/pass hash, true creates the hash
412 public boolean setBasicAuth(boolean useBasic) {
414 logger.debug("Clearing out the stored BASIC auth now.");
417 } else if (!basicAuth.isEmpty()) {
418 // If the binding sends multiple requests before basicAuth was set, this may trigger falsely.
419 logger.warn("Camera is reporting your username and/or password is wrong.");
422 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
423 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
424 ByteBuf byteBuf = null;
426 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
427 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
429 if (byteBuf != null) {
435 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
440 private String getCorrectUrlFormat(String longUrl) {
441 String temp = longUrl;
444 if (longUrl.isEmpty() || "ffmpeg".equals(longUrl)) {
449 url = new URL(longUrl);
450 int port = url.getPort();
452 if (url.getQuery() == null) {
453 temp = url.getPath();
455 temp = url.getPath() + "?" + url.getQuery();
458 if (url.getQuery() == null) {
459 temp = ":" + url.getPort() + url.getPath();
461 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
464 } catch (MalformedURLException e) {
465 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
470 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
471 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
472 sendHttpRequest("PUT", httpRequestURL, null);
475 public void sendHttpPOST(String httpPostURL, String content) {
476 FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, httpPostURL);
477 request.headers().set("Host", cameraConfig.getIp());
478 request.headers().add("Content-Type", "application/json");
479 request.headers().add("User-Agent",
480 "openHAB/" + FrameworkUtil.getBundle(this.getClass()).getVersion().toString());
481 request.headers().add("Accept", "*/*");
482 ByteBuf bbuf = Unpooled.copiedBuffer(content, StandardCharsets.UTF_8);
483 request.headers().set("Content-Length", bbuf.readableBytes());
484 request.content().clear().writeBytes(bbuf);
485 postRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
486 sendHttpRequest("POST", httpPostURL, null);
489 public void sendHttpPOST(String httpRequestURL) {
490 sendHttpRequest("POST", httpRequestURL, null);
493 public void sendHttpGET(String httpRequestURL) {
494 sendHttpRequest("GET", httpRequestURL, null);
497 public int getPortFromShortenedUrl(String httpRequestURL) {
498 if (httpRequestURL.startsWith(":")) {
499 int end = httpRequestURL.indexOf("/");
500 return Integer.parseInt(httpRequestURL.substring(1, end));
502 return cameraConfig.getPort();
505 public String getTinyUrl(String httpRequestURL) {
506 if (httpRequestURL.startsWith(":")) {
507 int beginIndex = httpRequestURL.indexOf("/");
508 return httpRequestURL.substring(beginIndex);
510 return httpRequestURL;
513 private void checkCameraConnection() {
514 if (snapshotPolling) {// Currently polling a real URL for snapshots, so camera must be online.
516 } else if (ffmpegSnapshotGeneration) {// Use RTSP stream creating snapshots to know camera is online.
517 Ffmpeg localSnapshot = ffmpegSnapshot;
518 if (localSnapshot != null && !localSnapshot.getIsAlive()) {
519 cameraCommunicationError("FFmpeg Snapshots Stopped: Check your camera can be reached.");
522 return;// ffmpeg snapshot stream is still alive
524 // Open a HTTP connection without sending any requests as we do not need a snapshot.
525 Bootstrap localBootstrap = mainBootstrap;
526 if (localBootstrap != null) {
527 ChannelFuture chFuture = localBootstrap
528 .connect(new InetSocketAddress(cameraConfig.getIp(), cameraConfig.getPort()));
529 if (chFuture.awaitUninterruptibly(500)) {
530 chFuture.channel().close();
534 cameraCommunicationError(
535 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
538 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
539 // The authHandler will generate a digest string and re-send using this same function when needed.
540 @SuppressWarnings("null")
541 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
542 int port = getPortFromShortenedUrl(httpRequestURLFull);
543 String httpRequestURL = getTinyUrl(httpRequestURLFull);
545 if (mainBootstrap == null) {
546 mainBootstrap = new Bootstrap();
547 mainBootstrap.group(mainEventLoopGroup);
548 mainBootstrap.channel(NioSocketChannel.class);
549 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
550 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
551 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
552 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
553 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
554 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
557 public void initChannel(SocketChannel socketChannel) throws Exception {
558 // HIK Alarm stream needs > 9sec idle to stop stream closing
559 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
560 socketChannel.pipeline().addLast(new HttpClientCodec());
561 socketChannel.pipeline().addLast(AUTH_HANDLER,
562 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
563 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
565 switch (thing.getThingTypeUID().getId()) {
567 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
570 socketChannel.pipeline()
571 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
574 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
577 socketChannel.pipeline().addLast(
578 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
580 case HIKVISION_THING:
581 socketChannel.pipeline()
582 .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
585 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
588 socketChannel.pipeline().addLast(REOLINK_HANDLER, new ReolinkHandler(getHandle()));
591 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
598 FullHttpRequest request;
599 if ("GET".equals(httpMethod) || (useDigestAuth && digestString == null)) {
600 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
601 request.headers().set("Host", cameraConfig.getIp() + ":" + port);
602 request.headers().set("Connection", HttpHeaderValues.CLOSE);
603 } else if ("PUT".equals(httpMethod)) {
604 request = putRequestWithBody;
606 request = postRequestWithBody;
609 if (!basicAuth.isEmpty()) {
611 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
614 request.headers().set("Authorization", "Basic " + basicAuth);
619 if (digestString != null) {
620 request.headers().set("Authorization", "Digest " + digestString);
624 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
625 .addListener(new ChannelFutureListener() {
628 public void operationComplete(@Nullable ChannelFuture future) {
629 if (future == null) {
632 if (future.isDone() && future.isSuccess()) {
633 Channel ch = future.channel();
634 openChannels.add(ch);
638 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
641 openChannel(ch, httpRequestURL);
642 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
643 commonHandler.setURL(httpRequestURLFull);
644 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
645 authHandler.setURL(httpMethod, httpRequestURL);
647 switch (thing.getThingTypeUID().getId()) {
649 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
650 amcrestHandler.setURL(httpRequestURL);
653 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
654 instarHandler.setURL(httpRequestURL);
657 ReolinkHandler reolinkHandler = (ReolinkHandler) ch.pipeline().get(REOLINK_HANDLER);
658 reolinkHandler.setURL(httpRequestURL);
661 ch.writeAndFlush(request);
662 } else { // an error occured
663 cameraCommunicationError(
664 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
670 public void processSnapshot(byte[] incommingSnapshot) {
671 lockCurrentSnapshot.lock();
673 currentSnapshot = incommingSnapshot;
674 if (cameraConfig.getGifPreroll() > 0) {
675 fifoSnapshotBuffer.add(incommingSnapshot);
676 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
677 fifoSnapshotBuffer.removeFirst();
681 lockCurrentSnapshot.unlock();
682 currentSnapshotTime = Instant.now();
685 if (updateImageChannel) {
686 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
687 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
688 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
689 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
690 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
691 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
692 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
696 public void startStreamServer() {
697 servlet = new CameraServlet(this, httpService);
698 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
699 + getThing().getUID().getId() + "/ipcamera.m3u8"));
700 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
701 + getThing().getUID().getId() + "/ipcamera.jpg"));
702 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
703 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
706 public void openCamerasStream() {
707 if (mjpegUri.isEmpty() || "ffmpeg".equals(mjpegUri)) {
708 setupFfmpegFormat(FFmpegFormat.MJPEG);
711 closeChannel(getTinyUrl(mjpegUri));
712 // Dahua cameras crash if you refresh (close and open) the stream without this delay.
713 mainEventLoopGroup.schedule(this::openMjpegStream, 300, TimeUnit.MILLISECONDS);
716 private void openMjpegStream() {
717 sendHttpGET(mjpegUri);
720 private void openChannel(Channel channel, String httpRequestURL) {
721 ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
722 if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
723 tracker.setChannel(channel);
726 channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
729 public void closeChannel(String url) {
730 ChannelTracking channelTracking = channelTrackingMap.get(url);
731 if (channelTracking != null) {
732 if (channelTracking.getChannel().isOpen()) {
733 channelTracking.getChannel().close();
740 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
741 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
744 private void cleanChannels() {
745 for (Channel channel : openChannels) {
746 boolean oldChannel = true;
747 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
748 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
749 channelTrackingMap.remove(channelTracking.getRequestUrl());
751 if (channelTracking.getChannel().equals(channel)) {
752 logger.debug("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
762 public void storeHttpReply(String url, String content) {
763 ChannelTracking channelTracking = channelTrackingMap.get(url);
764 if (channelTracking != null) {
765 channelTracking.setReply(content);
769 private void storeSnapshots() {
771 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
772 lockCurrentSnapshot.lock();
774 for (byte[] foo : fifoSnapshotBuffer) {
775 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
778 OutputStream fos = new FileOutputStream(file);
781 } catch (FileNotFoundException e) {
782 logger.warn("FileNotFoundException {}", e.getMessage());
783 } catch (IOException e) {
784 logger.warn("IOException {}", e.getMessage());
788 lockCurrentSnapshot.unlock();
792 public void setupFfmpegFormat(FFmpegFormat format) {
793 String inputOptions = cameraConfig.getFfmpegInputOptions();
794 if (cameraConfig.getFfmpegOutput().isEmpty()) {
795 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
798 if (rtspUri.isEmpty()) {
799 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
802 if (cameraConfig.getFfmpegLocation().isEmpty()) {
803 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
806 if (rtspUri.toLowerCase().contains("rtsp")) {
807 if (inputOptions.isEmpty()) {
808 inputOptions = "-rtsp_transport tcp";
812 // Make sure the folder exists, if not create it.
813 new File(cameraConfig.getFfmpegOutput()).mkdirs();
816 if (ffmpegHLS == null) {
817 if (!inputOptions.isEmpty()) {
818 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
819 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
820 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
821 cameraConfig.getUser(), cameraConfig.getPassword());
823 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
824 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
825 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
826 cameraConfig.getPassword());
829 Ffmpeg localHLS = ffmpegHLS;
830 if (localHLS != null) {
831 localHLS.startConverting();
835 if (cameraConfig.getGifPreroll() > 0) {
836 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
837 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
838 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
839 + cameraConfig.getGifOutOptions(),
840 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
841 cameraConfig.getPassword());
843 if (!inputOptions.isEmpty()) {
844 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
846 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
848 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
849 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
850 cameraConfig.getUser(), cameraConfig.getPassword());
852 if (cameraConfig.getGifPreroll() > 0) {
855 Ffmpeg localGIF = ffmpegGIF;
856 if (localGIF != null) {
857 localGIF.startConverting();
858 if (gifHistory.isEmpty()) {
859 gifHistory = gifFilename;
860 } else if (!"ipcamera".equals(gifFilename)) {
861 gifHistory = gifFilename + "," + gifHistory;
862 if (gifHistoryLength > 49) {
863 int endIndex = gifHistory.lastIndexOf(",");
864 gifHistory = gifHistory.substring(0, endIndex);
867 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
871 if (!inputOptions.isEmpty()) {
872 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
874 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
876 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
877 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
878 cameraConfig.getUser(), cameraConfig.getPassword());
879 Ffmpeg localRecord = ffmpegRecord;
880 if (localRecord != null) {
881 localRecord.startConverting();
882 if (mp4History.isEmpty()) {
883 mp4History = mp4Filename;
884 } else if (!"ipcamera".equals(mp4Filename)) {
885 mp4History = mp4Filename + "," + mp4History;
886 if (mp4HistoryLength > 49) {
887 int endIndex = mp4History.lastIndexOf(",");
888 mp4History = mp4History.substring(0, endIndex);
892 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
895 Ffmpeg localAlarms = ffmpegRtspHelper;
896 if (localAlarms != null) {
897 localAlarms.stopConverting();
898 if (!ffmpegAudioAlarmEnabled && !ffmpegMotionAlarmEnabled) {
902 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
903 String filterOptions = "";
904 if (!ffmpegAudioAlarmEnabled) {
905 filterOptions = "-an";
907 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
909 if (!ffmpegMotionAlarmEnabled && !ffmpegSnapshotGeneration) {
910 filterOptions = filterOptions.concat(" -vn");
911 } else if (ffmpegMotionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
912 String usersMotionOptions = cameraConfig.getMotionOptions();
913 if (usersMotionOptions.startsWith("-")) {
914 // Need to put the users custom options first in the chain before the motion is detected
915 filterOptions += " " + usersMotionOptions + ",select='gte(scene,"
916 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
918 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
919 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
921 } else if (ffmpegMotionAlarmEnabled) {
922 filterOptions = filterOptions.concat(" -vf select='gte(scene,"
923 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print");
925 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
926 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
927 localAlarms = ffmpegRtspHelper;
928 if (localAlarms != null) {
929 localAlarms.startConverting();
933 if (ffmpegMjpeg == null) {
934 if (inputOptions.isEmpty()) {
935 inputOptions = "-hide_banner";
937 inputOptions += " -hide_banner";
939 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
940 cameraConfig.getMjpegOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
941 + getThing().getUID().getId() + "/ipcamera.jpg",
942 cameraConfig.getUser(), cameraConfig.getPassword());
944 Ffmpeg localMjpeg = ffmpegMjpeg;
945 if (localMjpeg != null) {
946 localMjpeg.startConverting();
950 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
951 if (ffmpegSnapshot == null) {
952 if (inputOptions.isEmpty()) {
954 inputOptions = "-threads 1 -skip_frame nokey -hide_banner";
956 inputOptions += " -threads 1 -skip_frame nokey -hide_banner";
958 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
959 cameraConfig.getSnapshotOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
960 + getThing().getUID().getId() + "/snapshot.jpg",
961 cameraConfig.getUser(), cameraConfig.getPassword());
963 Ffmpeg localSnaps = ffmpegSnapshot;
964 if (localSnaps != null) {
965 localSnaps.startConverting();
971 public void noMotionDetected(String thisAlarmsChannel) {
972 setChannelState(thisAlarmsChannel, OnOffType.OFF);
973 firstMotionAlarm = false;
974 motionAlarmUpdateSnapshot = false;
975 motionDetected = false;
976 if (streamingAutoFps) {
977 stopSnapshotPolling();
978 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
979 stopSnapshotPolling();
984 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
985 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
986 * tampering with the camera.
988 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
989 updateState(thisAlarmsChannel, state);
992 public void motionDetected(String thisAlarmsChannel) {
993 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
994 updateState(thisAlarmsChannel, OnOffType.ON);
995 motionDetected = true;
996 if (streamingAutoFps) {
997 startSnapshotPolling();
999 if (cameraConfig.getUpdateImageWhen().contains("2")) {
1000 if (!firstMotionAlarm) {
1001 if (!snapshotUri.isEmpty()) {
1004 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1006 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1007 if (!snapshotPolling) {
1008 startSnapshotPolling();
1010 firstMotionAlarm = true;
1011 motionAlarmUpdateSnapshot = true;
1015 public void audioDetected() {
1016 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1017 if (cameraConfig.getUpdateImageWhen().contains("3")) {
1018 if (!firstAudioAlarm) {
1019 if (!snapshotUri.isEmpty()) {
1022 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1024 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1025 firstAudioAlarm = true;
1026 audioAlarmUpdateSnapshot = true;
1030 public void noAudioDetected() {
1031 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1032 firstAudioAlarm = false;
1033 audioAlarmUpdateSnapshot = false;
1036 public void recordMp4(String filename, int seconds) {
1037 mp4Filename = filename;
1038 mp4RecordTime = seconds;
1039 setupFfmpegFormat(FFmpegFormat.RECORD);
1040 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1043 public void recordGif(String filename, int seconds) {
1044 gifFilename = filename;
1045 gifRecordTime = seconds;
1046 if (cameraConfig.getGifPreroll() > 0) {
1047 snapCount = seconds;
1049 setupFfmpegFormat(FFmpegFormat.GIF);
1051 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1054 private void getReolinkToken() {
1055 sendHttpPOST("/api.cgi?cmd=Login",
1056 "[{\"cmd\":\"Login\", \"param\":{ \"User\":{ \"Version\": \"0\", \"userName\":\""
1057 + cameraConfig.getUser() + "\", \"password\":\"" + cameraConfig.getPassword() + "\"}}}]");
1060 public String returnValueFromString(String rawString, String searchedString) {
1062 int index = rawString.indexOf(searchedString);
1063 if (index != -1) // -1 means "not found"
1065 result = rawString.substring(index + searchedString.length(), rawString.length());
1066 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1068 return result; // Did not find a carriage return.
1070 return result.substring(0, index);
1073 return ""; // Did not find the String we were searching for
1076 private void sendPTZRequest() {
1077 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1081 public void channelLinked(ChannelUID channelUID) {
1082 switch (channelUID.getId()) {
1083 case CHANNEL_MJPEG_URL:
1084 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1085 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
1087 case CHANNEL_HLS_URL:
1088 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1089 + getThing().getUID().getId() + "/ipcamera.m3u8"));
1091 case CHANNEL_IMAGE_URL:
1092 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1093 + getThing().getUID().getId() + "/ipcamera.jpg"));
1098 public void removeChannels(List<org.openhab.core.thing.Channel> removeChannels) {
1099 if (!removeChannels.isEmpty()) {
1100 ThingBuilder thingBuilder = editThing();
1101 thingBuilder.withoutChannels(removeChannels);
1102 updateThing(thingBuilder.build());
1107 public void handleCommand(ChannelUID channelUID, Command command) {
1108 if (command instanceof RefreshType) {
1109 switch (channelUID.getId()) {
1111 if (onvifCamera.supportsPTZ()) {
1112 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1116 if (onvifCamera.supportsPTZ()) {
1117 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1121 if (onvifCamera.supportsPTZ()) {
1122 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1125 case CHANNEL_GOTO_PRESET:
1126 if (onvifCamera.supportsPTZ()) {
1127 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1131 } // caution "REFRESH" can still progress to brand Handlers below the else.
1133 switch (channelUID.getId()) {
1134 case CHANNEL_MP4_HISTORY_LENGTH:
1135 if (DecimalType.ZERO.equals(command)) {
1136 mp4HistoryLength = 0;
1138 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1141 case CHANNEL_GIF_HISTORY_LENGTH:
1142 if (DecimalType.ZERO.equals(command)) {
1143 gifHistoryLength = 0;
1145 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1148 case CHANNEL_FFMPEG_MOTION_CONTROL:
1149 if (OnOffType.ON.equals(command)) {
1150 ffmpegMotionAlarmEnabled = true;
1151 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1152 ffmpegMotionAlarmEnabled = false;
1153 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1154 } else if (command instanceof PercentType percentCommand) {
1155 ffmpegMotionAlarmEnabled = true;
1156 motionThreshold = percentCommand.toBigDecimal();
1158 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1160 case CHANNEL_START_STREAM:
1162 if (OnOffType.ON.equals(command)) {
1163 localHLS = ffmpegHLS;
1164 if (localHLS == null) {
1165 setupFfmpegFormat(FFmpegFormat.HLS);
1166 localHLS = ffmpegHLS;
1168 if (localHLS != null) {
1169 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1170 localHLS.startConverting();
1173 localHLS = ffmpegHLS;
1174 if (localHLS != null) {
1175 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1176 localHLS.setKeepAlive(1);
1180 case CHANNEL_EXTERNAL_MOTION:
1181 if (OnOffType.ON.equals(command)) {
1182 motionDetected(CHANNEL_EXTERNAL_MOTION);
1184 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1187 case CHANNEL_GOTO_PRESET:
1188 if (onvifCamera.supportsPTZ()) {
1189 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1192 case CHANNEL_POLL_IMAGE:
1193 if (OnOffType.ON.equals(command)) {
1194 if (snapshotUri.isEmpty()) {
1195 ffmpegSnapshotGeneration = true;
1196 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1197 updateImageChannel = false;
1199 updateImageChannel = true;
1200 updateSnapshot();// Allows this to change Image FPS on demand
1203 Ffmpeg localSnaps = ffmpegSnapshot;
1204 if (localSnaps != null) {
1205 localSnaps.stopConverting();
1206 ffmpegSnapshotGeneration = false;
1208 updateImageChannel = false;
1212 if (onvifCamera.supportsPTZ()) {
1213 if (command instanceof IncreaseDecreaseType) {
1214 if (command == IncreaseDecreaseType.INCREASE) {
1215 if (cameraConfig.getPtzContinuous()) {
1216 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1218 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1221 if (cameraConfig.getPtzContinuous()) {
1222 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1224 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1228 } else if (OnOffType.OFF.equals(command)) {
1229 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1232 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1233 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1237 if (onvifCamera.supportsPTZ()) {
1238 if (command instanceof IncreaseDecreaseType) {
1239 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1240 if (cameraConfig.getPtzContinuous()) {
1241 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1243 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1246 if (cameraConfig.getPtzContinuous()) {
1247 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1249 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1253 } else if (OnOffType.OFF.equals(command)) {
1254 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1257 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1258 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1262 if (onvifCamera.supportsPTZ()) {
1263 if (command instanceof IncreaseDecreaseType) {
1264 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1265 if (cameraConfig.getPtzContinuous()) {
1266 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1268 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1271 if (cameraConfig.getPtzContinuous()) {
1272 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1274 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1278 } else if (OnOffType.OFF.equals(command)) {
1279 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1282 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1283 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1288 // commands and refresh now get passed to brand handlers
1289 switch (thing.getThingTypeUID().getId()) {
1291 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1292 amcrestHandler.handleCommand(channelUID, command);
1293 if (lowPriorityRequests.isEmpty()) {
1294 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1298 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1299 dahuaHandler.handleCommand(channelUID, command);
1300 if (lowPriorityRequests.isEmpty()) {
1301 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1304 case DOORBIRD_THING:
1305 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1306 doorBirdHandler.handleCommand(channelUID, command);
1307 if (lowPriorityRequests.isEmpty()) {
1308 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1311 case HIKVISION_THING:
1312 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1313 hikvisionHandler.handleCommand(channelUID, command);
1316 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1317 cameraConfig.getPassword());
1318 foscamHandler.handleCommand(channelUID, command);
1319 if (lowPriorityRequests.isEmpty()) {
1320 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1324 InstarHandler instarHandler = new InstarHandler(getHandle());
1325 instarHandler.handleCommand(channelUID, command);
1326 if (lowPriorityRequests.isEmpty()) {
1327 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1331 ReolinkHandler reolinkHandler = new ReolinkHandler(getHandle());
1332 reolinkHandler.handleCommand(channelUID, command);
1335 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1336 defaultHandler.handleCommand(channelUID, command);
1337 if (lowPriorityRequests.isEmpty()) {
1338 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1344 public void setChannelState(String channelToUpdate, State valueOf) {
1345 updateState(channelToUpdate, valueOf);
1348 private void bringCameraOnline() {
1350 updateStatus(ThingStatus.ONLINE);
1351 groupTracker.listOfOnlineCameraHandlers.add(this);
1352 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1353 Future<?> localFuture = cameraConnectionJob;
1354 if (localFuture != null) {
1355 localFuture.cancel(false);
1356 cameraConnectionJob = null;
1358 if (!snapshotUri.isEmpty()) {
1359 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1360 snapshotPolling = true;
1361 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000,
1362 cameraConfig.getPollTime(), TimeUnit.MILLISECONDS);
1366 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1368 // auto restart mjpeg stream now camera is back online.
1369 CameraServlet localServlet = servlet;
1370 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1371 openCamerasStream();
1374 if (!rtspUri.isEmpty()) {
1375 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1377 if (updateImageChannel) {
1378 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1380 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1382 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1383 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1384 handle.cameraOnline(getThing().getUID().getId());
1389 void snapshotIsFfmpeg() {
1390 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1392 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1393 bringCameraOnline();
1394 if (!rtspUri.isEmpty()) {
1395 updateImageChannel = false;
1396 ffmpegSnapshotGeneration = true;
1397 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1398 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1400 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1404 void pollingCameraConnection() {
1406 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)
1407 || thing.getThingTypeUID().getId().equals(DOORBIRD_THING)) {
1408 if (rtspUri.isEmpty()) {
1409 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1411 if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1414 ffmpegSnapshotGeneration = false;
1419 if (cameraConfig.getOnvifPort() > 0 && !onvifCamera.isConnected()) {
1420 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1421 cameraConfig.getOnvifPort());
1422 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1424 if ("ffmpeg".equals(snapshotUri)) {
1426 } else if (!snapshotUri.isEmpty()) {
1427 ffmpegSnapshotGeneration = false;
1429 } else if (!rtspUri.isEmpty()) {
1432 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1433 "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.");
1437 public void cameraConfigError(String reason) {
1438 // wont try to reconnect again due to a config error being the cause.
1439 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1443 public void cameraCommunicationError(String reason) {
1444 // will try to reconnect again as camera may be rebooting.
1445 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1446 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1447 resetAndRetryConnecting();
1451 private boolean streamIsStopped(String url) {
1452 ChannelTracking channelTracking = channelTrackingMap.get(url);
1453 if (channelTracking != null) {
1454 if (channelTracking.getChannel().isActive()) {
1455 return false; // stream is running.
1458 return true; // Stream stopped or never started.
1461 void snapshotRunnable() {
1462 // Snapshot should be first to keep consistent time between shots
1464 if (snapCount > 0) {
1465 if (--snapCount == 0) {
1466 setupFfmpegFormat(FFmpegFormat.GIF);
1471 private void takeSnapshot() {
1472 sendHttpGET(snapshotUri);
1475 private void updateSnapshot() {
1476 lastSnapshotRequest = Instant.now();
1477 mainEventLoopGroup.schedule(this::takeSnapshot, 0, TimeUnit.MILLISECONDS);
1480 public byte[] getSnapshot() {
1482 // Single gray pixel JPG to keep streams open when the camera goes offline so they dont stop.
1483 return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
1484 0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43,
1485 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
1486 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09,
1487 0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d,
1488 0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10,
1489 0x10, (byte) 0xff, (byte) 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
1490 (byte) 0xff, (byte) 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, (byte) 0xff, (byte) 0xda, 0x00, 0x08,
1491 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, (byte) 0xd2, (byte) 0xcf, 0x20, (byte) 0xff, (byte) 0xd9 };
1493 // Most cameras will return a 503 busy error if snapshot is faster than 1 second
1494 long lastUpdatedMs = Duration.between(lastSnapshotRequest, Instant.now()).toMillis();
1495 if (!snapshotPolling && !ffmpegSnapshotGeneration && lastUpdatedMs >= cameraConfig.getPollTime()) {
1498 lockCurrentSnapshot.lock();
1500 return currentSnapshot;
1502 lockCurrentSnapshot.unlock();
1506 public void stopSnapshotPolling() {
1507 Future<?> localFuture;
1508 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1509 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1510 snapshotPolling = false;
1511 localFuture = snapshotJob;
1512 if (localFuture != null) {
1513 localFuture.cancel(true);
1515 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1516 snapshotPolling = false;
1517 localFuture = snapshotJob;
1518 if (localFuture != null) {
1519 localFuture.cancel(true);
1524 public void startSnapshotPolling() {
1525 if (snapshotPolling || ffmpegSnapshotGeneration) {
1526 return; // Already polling or creating with FFmpeg from RTSP
1528 if (streamingSnapshotMjpeg || streamingAutoFps || cameraConfig.getUpdateImageWhen().contains("4")) {
1529 snapshotPolling = true;
1530 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 0, cameraConfig.getPollTime(),
1531 TimeUnit.MILLISECONDS);
1536 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep alarm
1537 * streams open and more.
1540 void pollCameraRunnable() {
1541 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1542 if (!lowPriorityRequests.isEmpty()) {
1543 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1544 lowPriorityCounter = 0;
1546 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1548 // what needs to be done every poll//
1549 switch (thing.getThingTypeUID().getId()) {
1551 if (!snapshotPolling) {
1552 checkCameraConnection();
1556 if (!snapshotPolling) {
1557 checkCameraConnection();
1559 if (!onvifCamera.isConnected()) {
1560 onvifCamera.connect(true);
1564 if (!snapshotPolling) {
1565 checkCameraConnection();
1567 noMotionDetected(CHANNEL_MOTION_ALARM);
1568 noMotionDetected(CHANNEL_PIR_ALARM);
1569 noMotionDetected(CHANNEL_HUMAN_ALARM);
1570 noMotionDetected(CHANNEL_CAR_ALARM);
1571 noMotionDetected(CHANNEL_ANIMAL_ALARM);
1574 case HIKVISION_THING:
1575 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1576 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1577 cameraConfig.getIp());
1578 sendHttpGET("/ISAPI/Event/notification/alertStream");
1582 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1583 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1586 if (cameraConfig.getNvrChannel() > 0) {
1587 sendHttpGET("/api.cgi?cmd=GetAiState&channel=" + cameraConfig.getNvrChannel() + "&user="
1588 + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword());
1589 sendHttpGET("/api.cgi?cmd=GetMdState&channel=" + cameraConfig.getNvrChannel() + "&user="
1590 + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword());
1592 if (!snapshotPolling) {
1593 checkCameraConnection();
1595 if (!onvifCamera.isConnected()) {
1596 onvifCamera.connect(true);
1601 if (!snapshotPolling) {
1602 checkCameraConnection();
1604 // Check for alarms, channel for NVRs appears not to work at filtering.
1605 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1606 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1607 cameraConfig.getIp());
1608 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1611 case DOORBIRD_THING:
1612 if (!snapshotPolling) {
1613 checkCameraConnection();
1615 // Check for alarms, channel for NVRs appears not to work at filtering.
1616 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1617 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1618 cameraConfig.getIp());
1619 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1623 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1624 + cameraConfig.getPassword());
1627 Ffmpeg localFfmpeg = ffmpegHLS;
1628 if (localFfmpeg != null) {
1629 localFfmpeg.checkKeepAlive();
1631 if (ffmpegMotionAlarmEnabled || ffmpegAudioAlarmEnabled) {
1632 localFfmpeg = ffmpegRtspHelper;
1633 if (localFfmpeg == null || !localFfmpeg.getIsAlive()) {
1634 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1637 // check if the thread has frozen due to camera doing a soft reboot
1638 localFfmpeg = ffmpegMjpeg;
1639 if (localFfmpeg != null && !localFfmpeg.getIsAlive()) {
1640 logger.debug("MJPEG was not being produced by FFmpeg when it should have been, restarting FFmpeg.");
1641 setupFfmpegFormat(FFmpegFormat.MJPEG);
1643 if (openChannels.size() > 10) {
1644 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1650 public void initialize() {
1651 cameraConfig = getConfigAs(CameraConfig.class);
1652 threadPool = Executors.newScheduledThreadPool(2);
1653 mainEventLoopGroup = new NioEventLoopGroup(3);
1654 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1655 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1656 rtspUri = cameraConfig.getFfmpegInput();
1657 if (cameraConfig.getFfmpegOutput().isEmpty()) {
1659 .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1661 // Known cameras will connect quicker if we skip ONVIF questions.
1662 switch (thing.getThingTypeUID().getId()) {
1665 if (mjpegUri.isEmpty()) {
1666 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1668 if (snapshotUri.isEmpty()) {
1669 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1672 case DOORBIRD_THING:
1673 if (mjpegUri.isEmpty()) {
1674 mjpegUri = "/bha-api/video.cgi";
1676 if (snapshotUri.isEmpty()) {
1677 snapshotUri = "/bha-api/image.cgi";
1681 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1682 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1683 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1684 if (mjpegUri.isEmpty()) {
1685 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1686 + cameraConfig.getPassword();
1688 if (snapshotUri.isEmpty()) {
1689 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1690 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1693 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1694 if (mjpegUri.isEmpty()) {
1695 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1697 if (snapshotUri.isEmpty()) {
1698 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1700 if (lowPriorityRequests.isEmpty()) {
1701 lowPriorityRequests.add("/ISAPI/System/IO/inputs/" + cameraConfig.getNvrChannel() + "/status");
1705 if (snapshotUri.isEmpty()) {
1706 snapshotUri = "/tmpfs/snap.jpg";
1708 if (mjpegUri.isEmpty()) {
1709 mjpegUri = "/mjpegstream.cgi?-chn=12";
1711 // Newer Instar cameras use this to setup the Alarm Server, plus it is used to work out which API is
1712 // implemented based on the response to these two requests.
1714 "/param.cgi?cmd=setasaction&-server=1&enable=1&-interval=1&cmd=setasattr&-as_index=1&-as_server="
1715 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1716 + getThing().getUID().getId()
1717 + "/instar&-as_ssl=0&-as_insecure=0&-as_mode=0&-as_activequery=1&-as_auth=0&-as_query1=0&-as_query2=0&-as_query3=0&-as_query4=0&-as_query5=0");
1718 // Older Instar cameras use this to setup the Alarm Server
1720 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
1721 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1722 + getThing().getUID().getId()
1723 + "/instar&-as_ssl=0&-as_mode=1&-as_activequery=1&-as_auth=0&-as_query1=0&-as_query2=0&-as_query3=0&-as_query4=0&-as_query5=0");
1726 if (cameraConfig.useToken) {
1727 authenticationJob = threadPool.scheduleWithFixedDelay(this::getReolinkToken, 0, 45,
1730 reolinkAuth = "&user=" + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword();
1732 if (snapshotUri.isEmpty()) {
1733 if (cameraConfig.getNvrChannel() < 1) {
1734 snapshotUri = "/cgi-bin/api.cgi?cmd=Snap&channel=0&rs=openHAB" + reolinkAuth;
1736 snapshotUri = "/cgi-bin/api.cgi?cmd=Snap&channel=" + (cameraConfig.getNvrChannel() - 1)
1737 + "&rs=openHAB" + reolinkAuth;
1740 if (rtspUri.isEmpty()) {
1741 if (cameraConfig.getNvrChannel() < 1) {
1742 rtspUri = "rtsp://" + cameraConfig.getIp() + ":554/h264Preview_01_main";
1744 rtspUri = "rtsp://" + cameraConfig.getIp() + ":554/h264Preview_0" + cameraConfig.getNvrChannel()
1750 // for poll times 9 seconds and above don't display a warning about the Image channel.
1751 if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1753 "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.");
1755 // ONVIF and Instar event handling need the server started before connecting.
1756 startStreamServer();
1760 private void tryConnecting() {
1761 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)
1762 && !thing.getThingTypeUID().getId().equals(DOORBIRD_THING) && cameraConfig.getOnvifPort() > 0) {
1763 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1764 cameraConfig.getUser(), cameraConfig.getPassword());
1765 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1766 // Only use ONVIF events if it is not an API camera.
1767 onvifCamera.connect(supportsOnvifEvents());
1769 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 8, TimeUnit.SECONDS);
1772 private boolean supportsOnvifEvents() {
1773 switch (thing.getThingTypeUID().getId()) {
1777 if (cameraConfig.getNvrChannel() < 1) {
1784 private void keepMjpegRunning() {
1785 CameraServlet localServlet = servlet;
1786 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1787 if (!mjpegUri.isEmpty() && !"ffmpeg".equals(mjpegUri)) {
1788 localServlet.openStreams.queueFrame(("--" + localServlet.openStreams.boundary + "\r\n\r\n").getBytes());
1790 localServlet.openStreams.queueFrame(getSnapshot());
1794 // What the camera needs to re-connect if the initialize() is not called.
1795 private void resetAndRetryConnecting() {
1800 private void offline() {
1802 snapshotPolling = false;
1803 Future<?> localFuture = pollCameraJob;
1804 if (localFuture != null) {
1805 localFuture.cancel(true);
1806 pollCameraJob = null;
1808 localFuture = authenticationJob;
1809 if (localFuture != null) {
1810 localFuture.cancel(true);
1811 authenticationJob = null;
1813 localFuture = snapshotJob;
1814 if (localFuture != null) {
1815 localFuture.cancel(true);
1818 localFuture = cameraConnectionJob;
1819 if (localFuture != null) {
1820 localFuture.cancel(true);
1821 cameraConnectionJob = null;
1823 Ffmpeg localFfmpeg = ffmpegHLS;
1824 if (localFfmpeg != null) {
1825 localFfmpeg.stopConverting();
1828 localFfmpeg = ffmpegRecord;
1829 if (localFfmpeg != null) {
1830 localFfmpeg.stopConverting();
1831 ffmpegRecord = null;
1833 localFfmpeg = ffmpegGIF;
1834 if (localFfmpeg != null) {
1835 localFfmpeg.stopConverting();
1838 localFfmpeg = ffmpegRtspHelper;
1839 if (localFfmpeg != null) {
1840 localFfmpeg.stopConverting();
1841 ffmpegRtspHelper = null;
1843 localFfmpeg = ffmpegMjpeg;
1844 if (localFfmpeg != null) {
1845 localFfmpeg.stopConverting();
1848 localFfmpeg = ffmpegSnapshot;
1849 if (localFfmpeg != null) {
1850 localFfmpeg.stopConverting();
1851 ffmpegSnapshot = null;
1853 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {// generic cameras do not have ONVIF support
1854 onvifCamera.disconnect();
1856 openChannels.close();
1860 public void dispose() {
1862 CameraServlet localServlet = servlet;
1863 if (localServlet != null) {
1864 localServlet.dispose();
1867 threadPool.shutdown();
1868 // inform all group handlers that this camera has gone offline
1869 groupTracker.listOfOnlineCameraHandlers.remove(this);
1870 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1871 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1872 handle.cameraOffline(this);
1874 basicAuth = ""; // clear out stored Password hash
1875 useDigestAuth = false;
1876 mainEventLoopGroup.shutdownGracefully();
1877 mainBootstrap = null;
1878 channelTrackingMap.clear();
1881 public String getWhiteList() {
1882 return cameraConfig.getIpWhitelist();
1886 public Collection<Class<? extends ThingHandlerService>> getServices() {
1887 return Set.of(IpCameraActions.class);