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
525 // if ONVIF cam also use connection state which is updated by regular messages to camera
526 if (thing.getThingTypeUID().getId().equals(ONVIF_THING) && snapshotUri.isEmpty() && onvifCamera.isConnected()) {
530 // Open a HTTP connection without sending any requests as we do not need a snapshot.
531 Bootstrap localBootstrap = mainBootstrap;
532 if (localBootstrap != null) {
533 ChannelFuture chFuture = localBootstrap
534 .connect(new InetSocketAddress(cameraConfig.getIp(), cameraConfig.getPort()));
535 if (chFuture.awaitUninterruptibly(500)) {
536 chFuture.channel().close();
540 cameraCommunicationError(
541 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
544 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
545 // The authHandler will generate a digest string and re-send using this same function when needed.
546 @SuppressWarnings("null")
547 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
548 int port = getPortFromShortenedUrl(httpRequestURLFull);
549 String httpRequestURL = getTinyUrl(httpRequestURLFull);
551 if (mainBootstrap == null) {
552 mainBootstrap = new Bootstrap();
553 mainBootstrap.group(mainEventLoopGroup);
554 mainBootstrap.channel(NioSocketChannel.class);
555 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
556 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
557 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
558 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
559 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
560 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
563 public void initChannel(SocketChannel socketChannel) throws Exception {
564 // HIK Alarm stream needs > 9sec idle to stop stream closing
565 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
566 socketChannel.pipeline().addLast(new HttpClientCodec());
567 socketChannel.pipeline().addLast(AUTH_HANDLER,
568 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
569 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
571 switch (thing.getThingTypeUID().getId()) {
573 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
576 socketChannel.pipeline()
577 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
580 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
583 socketChannel.pipeline().addLast(
584 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
586 case HIKVISION_THING:
587 socketChannel.pipeline()
588 .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
591 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
594 socketChannel.pipeline().addLast(REOLINK_HANDLER, new ReolinkHandler(getHandle()));
597 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
604 FullHttpRequest request;
605 if ("GET".equals(httpMethod) || (useDigestAuth && digestString == null)) {
606 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
607 request.headers().set("Host", cameraConfig.getIp() + ":" + port);
608 request.headers().set("Connection", HttpHeaderValues.CLOSE);
609 } else if ("PUT".equals(httpMethod)) {
610 request = putRequestWithBody;
612 request = postRequestWithBody;
615 if (!basicAuth.isEmpty()) {
617 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
620 request.headers().set("Authorization", "Basic " + basicAuth);
625 if (digestString != null) {
626 request.headers().set("Authorization", "Digest " + digestString);
630 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
631 .addListener(new ChannelFutureListener() {
634 public void operationComplete(@Nullable ChannelFuture future) {
635 if (future == null) {
638 if (future.isDone() && future.isSuccess()) {
639 Channel ch = future.channel();
640 openChannels.add(ch);
644 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
647 openChannel(ch, httpRequestURL);
648 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
649 commonHandler.setURL(httpRequestURLFull);
650 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
651 authHandler.setURL(httpMethod, httpRequestURL);
653 switch (thing.getThingTypeUID().getId()) {
655 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
656 amcrestHandler.setURL(httpRequestURL);
659 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
660 instarHandler.setURL(httpRequestURL);
663 ReolinkHandler reolinkHandler = (ReolinkHandler) ch.pipeline().get(REOLINK_HANDLER);
664 reolinkHandler.setURL(httpRequestURL);
667 ch.writeAndFlush(request);
668 } else { // an error occurred
669 cameraCommunicationError(
670 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
676 public void processSnapshot(byte[] incommingSnapshot) {
677 lockCurrentSnapshot.lock();
679 currentSnapshot = incommingSnapshot;
680 if (cameraConfig.getGifPreroll() > 0) {
681 fifoSnapshotBuffer.add(incommingSnapshot);
682 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
683 fifoSnapshotBuffer.removeFirst();
687 lockCurrentSnapshot.unlock();
688 currentSnapshotTime = Instant.now();
691 if (updateImageChannel) {
692 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
693 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
694 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
695 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
696 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
697 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
698 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
702 public void startStreamServer() {
703 servlet = new CameraServlet(this, httpService);
704 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
705 + getThing().getUID().getId() + "/ipcamera.m3u8"));
706 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
707 + getThing().getUID().getId() + "/ipcamera.jpg"));
708 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
709 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
712 public void openCamerasStream() {
713 if (mjpegUri.isEmpty() || "ffmpeg".equals(mjpegUri)) {
714 setupFfmpegFormat(FFmpegFormat.MJPEG);
717 closeChannel(getTinyUrl(mjpegUri));
718 // Dahua cameras crash if you refresh (close and open) the stream without this delay.
719 mainEventLoopGroup.schedule(this::openMjpegStream, 300, TimeUnit.MILLISECONDS);
722 private void openMjpegStream() {
723 sendHttpGET(mjpegUri);
726 private void openChannel(Channel channel, String httpRequestURL) {
727 ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
728 if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
729 tracker.setChannel(channel);
732 channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
735 public void closeChannel(String url) {
736 ChannelTracking channelTracking = channelTrackingMap.get(url);
737 if (channelTracking != null) {
738 if (channelTracking.getChannel().isOpen()) {
739 channelTracking.getChannel().close();
746 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
747 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
750 private void cleanChannels() {
751 for (Channel channel : openChannels) {
752 boolean oldChannel = true;
753 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
754 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
755 channelTrackingMap.remove(channelTracking.getRequestUrl());
757 if (channelTracking.getChannel().equals(channel)) {
758 logger.debug("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
768 public void storeHttpReply(String url, String content) {
769 ChannelTracking channelTracking = channelTrackingMap.get(url);
770 if (channelTracking != null) {
771 channelTracking.setReply(content);
775 private void storeSnapshots() {
777 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
778 lockCurrentSnapshot.lock();
780 for (byte[] foo : fifoSnapshotBuffer) {
781 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
784 OutputStream fos = new FileOutputStream(file);
787 } catch (FileNotFoundException e) {
788 logger.warn("FileNotFoundException {}", e.getMessage());
789 } catch (IOException e) {
790 logger.warn("IOException {}", e.getMessage());
794 lockCurrentSnapshot.unlock();
798 public void setupFfmpegFormat(FFmpegFormat format) {
799 String inputOptions = cameraConfig.getFfmpegInputOptions();
800 if (cameraConfig.getFfmpegOutput().isEmpty()) {
801 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
804 if (rtspUri.isEmpty()) {
805 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
808 if (cameraConfig.getFfmpegLocation().isEmpty()) {
809 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
812 if (rtspUri.toLowerCase().contains("rtsp")) {
813 if (inputOptions.isEmpty()) {
814 inputOptions = "-rtsp_transport tcp";
818 // Make sure the folder exists, if not create it.
819 new File(cameraConfig.getFfmpegOutput()).mkdirs();
822 if (ffmpegHLS == null) {
823 if (!inputOptions.isEmpty()) {
824 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
825 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
826 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
827 cameraConfig.getUser(), cameraConfig.getPassword());
829 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
830 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
831 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
832 cameraConfig.getPassword());
835 Ffmpeg localHLS = ffmpegHLS;
836 if (localHLS != null) {
837 localHLS.startConverting();
841 if (cameraConfig.getGifPreroll() > 0) {
842 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
843 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
844 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
845 + cameraConfig.getGifOutOptions(),
846 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
847 cameraConfig.getPassword());
849 if (!inputOptions.isEmpty()) {
850 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
852 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
854 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
855 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
856 cameraConfig.getUser(), cameraConfig.getPassword());
858 if (cameraConfig.getGifPreroll() > 0) {
861 Ffmpeg localGIF = ffmpegGIF;
862 if (localGIF != null) {
863 localGIF.startConverting();
864 if (gifHistory.isEmpty()) {
865 gifHistory = gifFilename;
866 } else if (!"ipcamera".equals(gifFilename)) {
867 gifHistory = gifFilename + "," + gifHistory;
868 if (gifHistoryLength > 49) {
869 int endIndex = gifHistory.lastIndexOf(",");
870 gifHistory = gifHistory.substring(0, endIndex);
873 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
877 if (!inputOptions.isEmpty()) {
878 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
880 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
882 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
883 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
884 cameraConfig.getUser(), cameraConfig.getPassword());
885 Ffmpeg localRecord = ffmpegRecord;
886 if (localRecord != null) {
887 localRecord.startConverting();
888 if (mp4History.isEmpty()) {
889 mp4History = mp4Filename;
890 } else if (!"ipcamera".equals(mp4Filename)) {
891 mp4History = mp4Filename + "," + mp4History;
892 if (mp4HistoryLength > 49) {
893 int endIndex = mp4History.lastIndexOf(",");
894 mp4History = mp4History.substring(0, endIndex);
898 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
901 Ffmpeg localAlarms = ffmpegRtspHelper;
902 if (localAlarms != null) {
903 localAlarms.stopConverting();
904 if (!ffmpegAudioAlarmEnabled && !ffmpegMotionAlarmEnabled) {
908 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
909 String filterOptions = "";
910 if (!ffmpegAudioAlarmEnabled) {
911 filterOptions = "-an";
913 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
915 if (!ffmpegMotionAlarmEnabled && !ffmpegSnapshotGeneration) {
916 filterOptions = filterOptions.concat(" -vn");
917 } else if (ffmpegMotionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
918 String usersMotionOptions = cameraConfig.getMotionOptions();
919 if (usersMotionOptions.startsWith("-")) {
920 // Need to put the users custom options first in the chain before the motion is detected
921 filterOptions += " " + usersMotionOptions + ",select='gte(scene,"
922 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
924 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
925 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
927 } else if (ffmpegMotionAlarmEnabled) {
928 filterOptions = filterOptions.concat(" -vf select='gte(scene,"
929 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print");
931 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
932 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
933 localAlarms = ffmpegRtspHelper;
934 if (localAlarms != null) {
935 localAlarms.startConverting();
939 if (ffmpegMjpeg == null) {
940 if (inputOptions.isEmpty()) {
941 inputOptions = "-hide_banner";
943 inputOptions += " -hide_banner";
945 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
946 cameraConfig.getMjpegOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
947 + getThing().getUID().getId() + "/ipcamera.jpg",
948 cameraConfig.getUser(), cameraConfig.getPassword());
950 Ffmpeg localMjpeg = ffmpegMjpeg;
951 if (localMjpeg != null) {
952 localMjpeg.startConverting();
956 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
957 if (ffmpegSnapshot == null) {
958 if (inputOptions.isEmpty()) {
960 inputOptions = "-threads 1 -skip_frame nokey -hide_banner";
962 inputOptions += " -threads 1 -skip_frame nokey -hide_banner";
964 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
965 cameraConfig.getSnapshotOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
966 + getThing().getUID().getId() + "/snapshot.jpg",
967 cameraConfig.getUser(), cameraConfig.getPassword());
969 Ffmpeg localSnaps = ffmpegSnapshot;
970 if (localSnaps != null) {
971 localSnaps.startConverting();
977 public void noMotionDetected(String thisAlarmsChannel) {
978 setChannelState(thisAlarmsChannel, OnOffType.OFF);
979 firstMotionAlarm = false;
980 motionAlarmUpdateSnapshot = false;
981 motionDetected = false;
982 if (streamingAutoFps) {
983 stopSnapshotPolling();
984 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
985 stopSnapshotPolling();
990 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
991 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
992 * tampering with the camera.
994 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
995 updateState(thisAlarmsChannel, state);
998 public void motionDetected(String thisAlarmsChannel) {
999 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
1000 updateState(thisAlarmsChannel, OnOffType.ON);
1001 motionDetected = true;
1002 if (streamingAutoFps) {
1003 startSnapshotPolling();
1005 if (cameraConfig.getUpdateImageWhen().contains("2")) {
1006 if (!firstMotionAlarm) {
1007 if (!snapshotUri.isEmpty()) {
1010 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1012 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1013 if (!snapshotPolling) {
1014 startSnapshotPolling();
1016 firstMotionAlarm = true;
1017 motionAlarmUpdateSnapshot = true;
1021 public void audioDetected() {
1022 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1023 if (cameraConfig.getUpdateImageWhen().contains("3")) {
1024 if (!firstAudioAlarm) {
1025 if (!snapshotUri.isEmpty()) {
1028 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1030 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1031 firstAudioAlarm = true;
1032 audioAlarmUpdateSnapshot = true;
1036 public void noAudioDetected() {
1037 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1038 firstAudioAlarm = false;
1039 audioAlarmUpdateSnapshot = false;
1042 public void recordMp4(String filename, int seconds) {
1043 mp4Filename = filename;
1044 mp4RecordTime = seconds;
1045 setupFfmpegFormat(FFmpegFormat.RECORD);
1046 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1049 public void recordGif(String filename, int seconds) {
1050 gifFilename = filename;
1051 gifRecordTime = seconds;
1052 if (cameraConfig.getGifPreroll() > 0) {
1053 snapCount = seconds;
1055 setupFfmpegFormat(FFmpegFormat.GIF);
1057 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1060 private void getReolinkToken() {
1061 sendHttpPOST("/api.cgi?cmd=Login",
1062 "[{\"cmd\":\"Login\", \"param\":{ \"User\":{ \"Version\": \"0\", \"userName\":\""
1063 + cameraConfig.getUser() + "\", \"password\":\"" + cameraConfig.getPassword() + "\"}}}]");
1066 public String returnValueFromString(String rawString, String searchedString) {
1068 int index = rawString.indexOf(searchedString);
1069 if (index != -1) // -1 means "not found"
1071 result = rawString.substring(index + searchedString.length(), rawString.length());
1072 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1074 return result; // Did not find a carriage return.
1076 return result.substring(0, index);
1079 return ""; // Did not find the String we were searching for
1082 private void sendPTZRequest() {
1083 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1087 public void channelLinked(ChannelUID channelUID) {
1088 switch (channelUID.getId()) {
1089 case CHANNEL_MJPEG_URL:
1090 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1091 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
1093 case CHANNEL_HLS_URL:
1094 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1095 + getThing().getUID().getId() + "/ipcamera.m3u8"));
1097 case CHANNEL_IMAGE_URL:
1098 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1099 + getThing().getUID().getId() + "/ipcamera.jpg"));
1104 public void removeChannels(List<org.openhab.core.thing.Channel> removeChannels) {
1105 if (!removeChannels.isEmpty()) {
1106 ThingBuilder thingBuilder = editThing();
1107 thingBuilder.withoutChannels(removeChannels);
1108 updateThing(thingBuilder.build());
1113 public void handleCommand(ChannelUID channelUID, Command command) {
1114 if (command instanceof RefreshType) {
1115 switch (channelUID.getId()) {
1117 if (onvifCamera.supportsPTZ()) {
1118 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1122 if (onvifCamera.supportsPTZ()) {
1123 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1127 if (onvifCamera.supportsPTZ()) {
1128 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1131 case CHANNEL_GOTO_PRESET:
1132 if (onvifCamera.supportsPTZ()) {
1133 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1137 } // caution "REFRESH" can still progress to brand Handlers below the else.
1139 switch (channelUID.getId()) {
1140 case CHANNEL_MP4_HISTORY_LENGTH:
1141 if (DecimalType.ZERO.equals(command)) {
1142 mp4HistoryLength = 0;
1144 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1147 case CHANNEL_GIF_HISTORY_LENGTH:
1148 if (DecimalType.ZERO.equals(command)) {
1149 gifHistoryLength = 0;
1151 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1154 case CHANNEL_FFMPEG_MOTION_CONTROL:
1155 if (OnOffType.ON.equals(command)) {
1156 ffmpegMotionAlarmEnabled = true;
1157 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1158 ffmpegMotionAlarmEnabled = false;
1159 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1160 } else if (command instanceof PercentType percentCommand) {
1161 ffmpegMotionAlarmEnabled = true;
1162 motionThreshold = percentCommand.toBigDecimal();
1164 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1166 case CHANNEL_START_STREAM:
1168 if (OnOffType.ON.equals(command)) {
1169 localHLS = ffmpegHLS;
1170 if (localHLS == null) {
1171 setupFfmpegFormat(FFmpegFormat.HLS);
1172 localHLS = ffmpegHLS;
1174 if (localHLS != null) {
1175 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1176 localHLS.startConverting();
1179 localHLS = ffmpegHLS;
1180 if (localHLS != null) {
1181 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1182 localHLS.setKeepAlive(1);
1186 case CHANNEL_EXTERNAL_MOTION:
1187 if (OnOffType.ON.equals(command)) {
1188 motionDetected(CHANNEL_EXTERNAL_MOTION);
1190 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1193 case CHANNEL_GOTO_PRESET:
1194 if (onvifCamera.supportsPTZ()) {
1195 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1198 case CHANNEL_POLL_IMAGE:
1199 if (OnOffType.ON.equals(command)) {
1200 if (snapshotUri.isEmpty()) {
1201 ffmpegSnapshotGeneration = true;
1202 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1203 updateImageChannel = false;
1205 updateImageChannel = true;
1206 updateSnapshot();// Allows this to change Image FPS on demand
1209 Ffmpeg localSnaps = ffmpegSnapshot;
1210 if (localSnaps != null) {
1211 localSnaps.stopConverting();
1212 ffmpegSnapshotGeneration = false;
1214 updateImageChannel = false;
1218 if (onvifCamera.supportsPTZ()) {
1219 if (command instanceof IncreaseDecreaseType) {
1220 if (command == IncreaseDecreaseType.INCREASE) {
1221 if (cameraConfig.getPtzContinuous()) {
1222 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1224 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1227 if (cameraConfig.getPtzContinuous()) {
1228 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1230 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1234 } else if (OnOffType.OFF.equals(command)) {
1235 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1238 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1239 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1243 if (onvifCamera.supportsPTZ()) {
1244 if (command instanceof IncreaseDecreaseType) {
1245 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1246 if (cameraConfig.getPtzContinuous()) {
1247 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1249 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1252 if (cameraConfig.getPtzContinuous()) {
1253 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1255 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1259 } else if (OnOffType.OFF.equals(command)) {
1260 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1263 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1264 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1268 if (onvifCamera.supportsPTZ()) {
1269 if (command instanceof IncreaseDecreaseType) {
1270 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1271 if (cameraConfig.getPtzContinuous()) {
1272 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1274 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1277 if (cameraConfig.getPtzContinuous()) {
1278 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1280 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1284 } else if (OnOffType.OFF.equals(command)) {
1285 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1288 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1289 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1294 // commands and refresh now get passed to brand handlers
1295 switch (thing.getThingTypeUID().getId()) {
1297 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1298 amcrestHandler.handleCommand(channelUID, command);
1299 if (lowPriorityRequests.isEmpty()) {
1300 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1304 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1305 dahuaHandler.handleCommand(channelUID, command);
1306 if (lowPriorityRequests.isEmpty()) {
1307 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1310 case DOORBIRD_THING:
1311 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1312 doorBirdHandler.handleCommand(channelUID, command);
1313 if (lowPriorityRequests.isEmpty()) {
1314 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1317 case HIKVISION_THING:
1318 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1319 hikvisionHandler.handleCommand(channelUID, command);
1322 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1323 cameraConfig.getPassword());
1324 foscamHandler.handleCommand(channelUID, command);
1325 if (lowPriorityRequests.isEmpty()) {
1326 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1330 InstarHandler instarHandler = new InstarHandler(getHandle());
1331 instarHandler.handleCommand(channelUID, command);
1332 if (lowPriorityRequests.isEmpty()) {
1333 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1337 ReolinkHandler reolinkHandler = new ReolinkHandler(getHandle());
1338 reolinkHandler.handleCommand(channelUID, command);
1341 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1342 defaultHandler.handleCommand(channelUID, command);
1343 if (lowPriorityRequests.isEmpty()) {
1344 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1350 public void setChannelState(String channelToUpdate, State valueOf) {
1351 updateState(channelToUpdate, valueOf);
1354 private void bringCameraOnline() {
1356 updateStatus(ThingStatus.ONLINE);
1357 groupTracker.listOfOnlineCameraHandlers.add(this);
1358 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1359 Future<?> localFuture = cameraConnectionJob;
1360 if (localFuture != null) {
1361 localFuture.cancel(false);
1362 cameraConnectionJob = null;
1364 if (!snapshotUri.isEmpty()) {
1365 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1366 snapshotPolling = true;
1367 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000,
1368 cameraConfig.getPollTime(), TimeUnit.MILLISECONDS);
1372 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1374 // auto restart mjpeg stream now camera is back online.
1375 CameraServlet localServlet = servlet;
1376 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1377 openCamerasStream();
1380 if (!rtspUri.isEmpty()) {
1381 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1383 if (updateImageChannel) {
1384 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1386 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1388 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1389 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1390 handle.cameraOnline(getThing().getUID().getId());
1395 void snapshotIsFfmpeg() {
1396 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1398 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1399 bringCameraOnline();
1400 if (!rtspUri.isEmpty()) {
1401 updateImageChannel = false;
1402 ffmpegSnapshotGeneration = true;
1403 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1404 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1406 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1410 void pollingCameraConnection() {
1412 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)
1413 || thing.getThingTypeUID().getId().equals(DOORBIRD_THING)) {
1414 if (rtspUri.isEmpty()) {
1415 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1417 if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1420 ffmpegSnapshotGeneration = false;
1425 if (cameraConfig.getOnvifPort() > 0 && !onvifCamera.isConnected()) {
1426 if (onvifCamera.isConnectError()) {
1427 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Camera is not reachable");
1428 } else if (onvifCamera.isRefusedError()) {
1429 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1430 "Camera refused connection on ONVIF ports.");
1432 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1433 cameraConfig.getOnvifPort());
1434 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1437 if ("ffmpeg".equals(snapshotUri)) {
1439 } else if (!snapshotUri.isEmpty()) {
1440 ffmpegSnapshotGeneration = false;
1442 } else if (!rtspUri.isEmpty()) {
1445 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1446 "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.");
1450 public void cameraConfigError(String reason) {
1451 // wont try to reconnect again due to a config error being the cause.
1452 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1456 public void cameraCommunicationError(String reason) {
1457 // will try to reconnect again as camera may be rebooting.
1458 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1459 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1460 resetAndRetryConnecting();
1464 private boolean streamIsStopped(String url) {
1465 ChannelTracking channelTracking = channelTrackingMap.get(url);
1466 if (channelTracking != null) {
1467 if (channelTracking.getChannel().isActive()) {
1468 return false; // stream is running.
1471 return true; // Stream stopped or never started.
1474 void snapshotRunnable() {
1475 // Snapshot should be first to keep consistent time between shots
1477 if (snapCount > 0) {
1478 if (--snapCount == 0) {
1479 setupFfmpegFormat(FFmpegFormat.GIF);
1484 private void takeSnapshot() {
1485 sendHttpGET(snapshotUri);
1488 private void updateSnapshot() {
1489 lastSnapshotRequest = Instant.now();
1490 mainEventLoopGroup.schedule(this::takeSnapshot, 0, TimeUnit.MILLISECONDS);
1493 public byte[] getSnapshot() {
1495 // Single gray pixel JPG to keep streams open when the camera goes offline so they dont stop.
1496 return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
1497 0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43,
1498 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
1499 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09,
1500 0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d,
1501 0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10,
1502 0x10, (byte) 0xff, (byte) 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
1503 (byte) 0xff, (byte) 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, (byte) 0xff, (byte) 0xda, 0x00, 0x08,
1504 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, (byte) 0xd2, (byte) 0xcf, 0x20, (byte) 0xff, (byte) 0xd9 };
1506 // Most cameras will return a 503 busy error if snapshot is faster than 1 second
1507 long lastUpdatedMs = Duration.between(lastSnapshotRequest, Instant.now()).toMillis();
1508 if (!snapshotPolling && !ffmpegSnapshotGeneration && lastUpdatedMs >= cameraConfig.getPollTime()) {
1511 lockCurrentSnapshot.lock();
1513 return currentSnapshot;
1515 lockCurrentSnapshot.unlock();
1519 public void stopSnapshotPolling() {
1520 Future<?> localFuture;
1521 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1522 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1523 snapshotPolling = false;
1524 localFuture = snapshotJob;
1525 if (localFuture != null) {
1526 localFuture.cancel(true);
1528 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1529 snapshotPolling = false;
1530 localFuture = snapshotJob;
1531 if (localFuture != null) {
1532 localFuture.cancel(true);
1537 public void startSnapshotPolling() {
1538 if (snapshotPolling || ffmpegSnapshotGeneration) {
1539 return; // Already polling or creating with FFmpeg from RTSP
1541 if (streamingSnapshotMjpeg || streamingAutoFps || cameraConfig.getUpdateImageWhen().contains("4")) {
1542 snapshotPolling = true;
1543 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 0, cameraConfig.getPollTime(),
1544 TimeUnit.MILLISECONDS);
1549 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep alarm
1550 * streams open and more.
1553 void pollCameraRunnable() {
1554 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1555 if (!lowPriorityRequests.isEmpty()) {
1556 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1557 lowPriorityCounter = 0;
1559 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1561 // what needs to be done every poll//
1562 switch (thing.getThingTypeUID().getId()) {
1564 if (!snapshotPolling) {
1565 checkCameraConnection();
1569 if (!snapshotPolling) {
1570 checkCameraConnection();
1574 if (!snapshotPolling) {
1575 checkCameraConnection();
1577 noMotionDetected(CHANNEL_MOTION_ALARM);
1578 noMotionDetected(CHANNEL_PIR_ALARM);
1579 noMotionDetected(CHANNEL_HUMAN_ALARM);
1580 noMotionDetected(CHANNEL_CAR_ALARM);
1581 noMotionDetected(CHANNEL_ANIMAL_ALARM);
1584 case HIKVISION_THING:
1585 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1586 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1587 cameraConfig.getIp());
1588 sendHttpGET("/ISAPI/Event/notification/alertStream");
1592 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1593 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1596 if (cameraConfig.getNvrChannel() > 0) {
1597 sendHttpGET("/api.cgi?cmd=GetAiState&channel=" + cameraConfig.getNvrChannel() + "&user="
1598 + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword());
1599 sendHttpGET("/api.cgi?cmd=GetMdState&channel=" + cameraConfig.getNvrChannel() + "&user="
1600 + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword());
1602 if (!snapshotPolling) {
1603 checkCameraConnection();
1605 if (!onvifCamera.isConnected()) {
1606 onvifCamera.connect(true);
1611 if (!snapshotPolling) {
1612 checkCameraConnection();
1614 // Check for alarms, channel for NVRs appears not to work at filtering.
1615 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1616 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1617 cameraConfig.getIp());
1618 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1621 case DOORBIRD_THING:
1622 if (!snapshotPolling) {
1623 checkCameraConnection();
1625 // Check for alarms, channel for NVRs appears not to work at filtering.
1626 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1627 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1628 cameraConfig.getIp());
1629 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1633 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1634 + cameraConfig.getPassword());
1637 Ffmpeg localFfmpeg = ffmpegHLS;
1638 if (localFfmpeg != null) {
1639 localFfmpeg.checkKeepAlive();
1641 if (ffmpegMotionAlarmEnabled || ffmpegAudioAlarmEnabled) {
1642 localFfmpeg = ffmpegRtspHelper;
1643 if (localFfmpeg == null || !localFfmpeg.getIsAlive()) {
1644 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1647 // check if the thread has frozen due to camera doing a soft reboot
1648 localFfmpeg = ffmpegMjpeg;
1649 if (localFfmpeg != null && !localFfmpeg.getIsAlive()) {
1650 logger.debug("MJPEG was not being produced by FFmpeg when it should have been, restarting FFmpeg.");
1651 setupFfmpegFormat(FFmpegFormat.MJPEG);
1653 if (openChannels.size() > 10) {
1654 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1660 public void initialize() {
1661 cameraConfig = getConfigAs(CameraConfig.class);
1662 threadPool = Executors.newScheduledThreadPool(2);
1663 mainEventLoopGroup = new NioEventLoopGroup(3);
1664 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1665 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1666 rtspUri = cameraConfig.getFfmpegInput();
1667 if (cameraConfig.getFfmpegOutput().isEmpty()) {
1669 .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1671 // Known cameras will connect quicker if we skip ONVIF questions.
1672 switch (thing.getThingTypeUID().getId()) {
1675 if (mjpegUri.isEmpty()) {
1676 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1678 if (snapshotUri.isEmpty()) {
1679 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1682 case DOORBIRD_THING:
1683 if (mjpegUri.isEmpty()) {
1684 mjpegUri = "/bha-api/video.cgi";
1686 if (snapshotUri.isEmpty()) {
1687 snapshotUri = "/bha-api/image.cgi";
1691 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1692 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1693 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1694 if (mjpegUri.isEmpty()) {
1695 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1696 + cameraConfig.getPassword();
1698 if (snapshotUri.isEmpty()) {
1699 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1700 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1703 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1704 if (mjpegUri.isEmpty()) {
1705 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1707 if (snapshotUri.isEmpty()) {
1708 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1710 if (lowPriorityRequests.isEmpty()) {
1711 lowPriorityRequests.add("/ISAPI/System/IO/inputs/" + cameraConfig.getNvrChannel() + "/status");
1715 if (snapshotUri.isEmpty()) {
1716 snapshotUri = "/tmpfs/snap.jpg";
1718 if (mjpegUri.isEmpty()) {
1719 mjpegUri = "/mjpegstream.cgi?-chn=12";
1721 // Newer Instar cameras use this to setup the Alarm Server, plus it is used to work out which API is
1722 // implemented based on the response to these two requests.
1724 "/param.cgi?cmd=setasaction&-server=1&enable=1&-interval=1&cmd=setasattr&-as_index=1&-as_server="
1725 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1726 + getThing().getUID().getId()
1727 + "/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");
1728 // Older Instar cameras use this to setup the Alarm Server
1730 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
1731 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1732 + getThing().getUID().getId()
1733 + "/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");
1736 if (cameraConfig.useToken) {
1737 authenticationJob = threadPool.scheduleWithFixedDelay(this::getReolinkToken, 0, 45,
1740 reolinkAuth = "&user=" + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword();
1742 if (snapshotUri.isEmpty()) {
1743 if (cameraConfig.getNvrChannel() < 1) {
1744 snapshotUri = "/cgi-bin/api.cgi?cmd=Snap&channel=0&rs=openHAB" + reolinkAuth;
1746 snapshotUri = "/cgi-bin/api.cgi?cmd=Snap&channel=" + (cameraConfig.getNvrChannel() - 1)
1747 + "&rs=openHAB" + reolinkAuth;
1750 if (rtspUri.isEmpty()) {
1751 if (cameraConfig.getNvrChannel() < 1) {
1752 rtspUri = "rtsp://" + cameraConfig.getIp() + ":554/h264Preview_01_main";
1754 rtspUri = "rtsp://" + cameraConfig.getIp() + ":554/h264Preview_0" + cameraConfig.getNvrChannel()
1760 // for poll times 9 seconds and above don't display a warning about the Image channel.
1761 if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1763 "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.");
1765 // ONVIF and Instar event handling need the server started before connecting.
1766 startStreamServer();
1770 private void tryConnecting() {
1772 int normalDelay = 12; // doesn't make sense to have faster retry than CONNECT_TIMEOUT, which is 10 seconds, if
1774 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)
1775 && !thing.getThingTypeUID().getId().equals(DOORBIRD_THING) && cameraConfig.getOnvifPort() > 0) {
1776 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1777 cameraConfig.getUser(), cameraConfig.getPassword());
1778 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1779 // Only use ONVIF events if it is not an API camera.
1780 onvifCamera.connect(supportsOnvifEvents());
1782 if (supportsOnvifEvents()) {
1783 // it takes some time to try to retrieve the ONVIF snapshot and stream URLs and update internal members
1784 // on first connect; if connection lost, doesn't make sense to poll to often
1789 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, firstDelay, normalDelay,
1793 private boolean supportsOnvifEvents() {
1794 switch (thing.getThingTypeUID().getId()) {
1798 if (cameraConfig.getNvrChannel() < 1) {
1805 private void keepMjpegRunning() {
1806 CameraServlet localServlet = servlet;
1807 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1808 if (!mjpegUri.isEmpty() && !"ffmpeg".equals(mjpegUri)) {
1809 localServlet.openStreams.queueFrame(("--" + localServlet.openStreams.boundary + "\r\n\r\n").getBytes());
1811 localServlet.openStreams.queueFrame(getSnapshot());
1815 // What the camera needs to re-connect if the initialize() is not called.
1816 private void resetAndRetryConnecting() {
1821 private void offline() {
1823 snapshotPolling = false;
1824 Future<?> localFuture = pollCameraJob;
1825 if (localFuture != null) {
1826 localFuture.cancel(true);
1827 pollCameraJob = null;
1829 localFuture = authenticationJob;
1830 if (localFuture != null) {
1831 localFuture.cancel(true);
1832 authenticationJob = null;
1834 localFuture = snapshotJob;
1835 if (localFuture != null) {
1836 localFuture.cancel(true);
1839 localFuture = cameraConnectionJob;
1840 if (localFuture != null) {
1841 localFuture.cancel(true);
1842 cameraConnectionJob = null;
1844 Ffmpeg localFfmpeg = ffmpegHLS;
1845 if (localFfmpeg != null) {
1846 localFfmpeg.stopConverting();
1849 localFfmpeg = ffmpegRecord;
1850 if (localFfmpeg != null) {
1851 localFfmpeg.stopConverting();
1852 ffmpegRecord = null;
1854 localFfmpeg = ffmpegGIF;
1855 if (localFfmpeg != null) {
1856 localFfmpeg.stopConverting();
1859 localFfmpeg = ffmpegRtspHelper;
1860 if (localFfmpeg != null) {
1861 localFfmpeg.stopConverting();
1862 ffmpegRtspHelper = null;
1864 localFfmpeg = ffmpegMjpeg;
1865 if (localFfmpeg != null) {
1866 localFfmpeg.stopConverting();
1869 localFfmpeg = ffmpegSnapshot;
1870 if (localFfmpeg != null) {
1871 localFfmpeg.stopConverting();
1872 ffmpegSnapshot = null;
1874 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {// generic cameras do not have ONVIF support
1875 onvifCamera.disconnect();
1877 openChannels.close();
1881 public void dispose() {
1883 CameraServlet localServlet = servlet;
1884 if (localServlet != null) {
1885 localServlet.dispose();
1888 threadPool.shutdown();
1889 // inform all group handlers that this camera has gone offline
1890 groupTracker.listOfOnlineCameraHandlers.remove(this);
1891 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1892 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1893 handle.cameraOffline(this);
1895 basicAuth = ""; // clear out stored Password hash
1896 useDigestAuth = false;
1897 mainEventLoopGroup.shutdownGracefully();
1898 mainBootstrap = null;
1899 channelTrackingMap.clear();
1902 public String getWhiteList() {
1903 return cameraConfig.getIpWhitelist();
1907 public Collection<Class<? extends ThingHandlerService>> getServices() {
1908 return Set.of(IpCameraActions.class);