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.Collections;
32 import java.util.LinkedList;
33 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) {
222 HttpResponse response = (HttpResponse) msg;
223 if (response.status().code() == 200) {
224 if (!response.headers().isEmpty()) {
225 for (String name : response.headers().names()) {
226 // Some cameras use first letter uppercase and others dont.
227 switch (name.toLowerCase()) { // Possible localization issues doing this
229 contentType = response.headers().getAsString(name);
231 case "content-length":
232 bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
234 case "transfer-encoding":
235 if (response.headers().getAsString(name).contains("chunked")) {
241 if (contentType.contains("multipart")) {
242 boundary = Helper.searchString(contentType, "boundary=");
243 if (mjpegUri.equals(requestUrl)) {
244 if (msg instanceof HttpMessage) {
245 // very start of stream only
246 mjpegContentType = contentType;
247 CameraServlet localServlet = servlet;
248 if (localServlet != null) {
249 logger.debug("Setting Content-Type to:{}", contentType);
250 localServlet.openStreams.updateContentType(contentType, boundary);
254 } else if (contentType.contains("image/jp")) {
255 if (bytesToRecieve == 0) {
256 bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
257 logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
259 incomingJpeg = new byte[bytesToRecieve];
263 // Non 200 OK replies are logged and handled in pipeline by MyNettyAuthHandler.java
267 if (msg instanceof HttpContent) {
268 HttpContent content = (HttpContent) msg;
269 if (mjpegUri.equals(requestUrl) && !(content instanceof LastHttpContent)) {
270 // multiple MJPEG stream packets come back as this.
271 byte[] chunkedFrame = new byte[content.content().readableBytes()];
272 content.content().getBytes(content.content().readerIndex(), chunkedFrame);
273 CameraServlet localServlet = servlet;
274 if (localServlet != null) {
275 localServlet.openStreams.queueFrame(chunkedFrame);
278 // Found some cameras use Content-Type: image/jpg instead of image/jpeg
279 if (contentType.contains("image/jp")) {
280 for (int i = 0; i < content.content().capacity(); i++) {
281 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
283 if (content instanceof LastHttpContent) {
284 processSnapshot(incomingJpeg);
287 } else { // incomingMessage that is not an IMAGE
288 if (incomingMessage.isEmpty()) {
289 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
291 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
293 bytesAlreadyRecieved = incomingMessage.length();
294 if (content instanceof LastHttpContent) {
295 // If it is not an image send it on to the next handler//
296 if (bytesAlreadyRecieved != 0) {
297 reply = incomingMessage;
298 super.channelRead(ctx, reply);
301 // Alarm Streams never have a LastHttpContent as they always stay open//
302 else if (contentType.contains("multipart")) {
303 int beginIndex, endIndex;
304 if (bytesToRecieve == 0) {
305 beginIndex = incomingMessage.indexOf("Content-Length:");
306 if (beginIndex != -1) {
307 endIndex = incomingMessage.indexOf("\r\n", beginIndex);
308 if (endIndex != -1) {
309 bytesToRecieve = Integer.parseInt(
310 incomingMessage.substring(beginIndex + 15, endIndex).strip());
314 // --boundary and headers are not included in the Content-Length value
315 if (bytesAlreadyRecieved > bytesToRecieve) {
316 // Check if message has a second --boundary
317 endIndex = incomingMessage.indexOf("--" + boundary, bytesToRecieve);
318 if (endIndex == -1) {
319 reply = incomingMessage;
320 incomingMessage = "";
322 bytesAlreadyRecieved = 0;
324 reply = incomingMessage.substring(0, endIndex);
325 incomingMessage = incomingMessage.substring(endIndex, incomingMessage.length());
326 bytesToRecieve = 0;// Triggers search next time for Content-Length:
327 bytesAlreadyRecieved = incomingMessage.length() - endIndex;
329 super.channelRead(ctx, reply);
332 // Foscam needs this as will other cameras with chunks//
333 if (isChunked && bytesAlreadyRecieved != 0) {
334 reply = incomingMessage;
338 } else { // msg is not HttpContent
339 // Foscam cameras need this
340 if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
341 reply = incomingMessage;
342 logger.trace("Packet back from camera is {}", incomingMessage);
343 super.channelRead(ctx, reply);
347 ReferenceCountUtil.release(msg);
352 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
353 if (cause == null || ctx == null) {
356 if (cause instanceof ArrayIndexOutOfBoundsException) {
357 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
360 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
367 @SuppressWarnings("PMD.CompareObjectsWithEquals")
368 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
372 if (evt instanceof IdleStateEvent) {
373 IdleStateEvent e = (IdleStateEvent) evt;
374 // If camera does not use the channel for X amount of time it will close.
375 if (e.state() == IdleState.READER_IDLE) {
376 String urlToKeepOpen = "";
377 switch (thing.getThingTypeUID().getId()) {
379 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
382 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
385 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
386 if (channelTracking != null) {
387 if (channelTracking.getChannel() == ctx.channel()) {
388 return; // don't auto close this as it is for the alarms.
391 logger.debug("Closing an idle channel for camera:{}", cameraConfig.getIp());
398 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
399 IpCameraDynamicStateDescriptionProvider stateDescriptionProvider, HttpService httpService) {
401 this.stateDescriptionProvider = stateDescriptionProvider;
402 if (ipAddress != null) {
405 hostIp = Helper.getLocalIpAddress();
407 this.groupTracker = groupTracker;
408 this.httpService = httpService;
411 private IpCameraHandler getHandle() {
415 // false clears the stored user/pass hash, true creates the hash
416 public boolean setBasicAuth(boolean useBasic) {
418 logger.debug("Clearing out the stored BASIC auth now.");
421 } else if (!basicAuth.isEmpty()) {
422 // If the binding sends multiple requests before basicAuth was set, this may trigger falsely.
423 logger.warn("Camera is reporting your username and/or password is wrong.");
426 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
427 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
428 ByteBuf byteBuf = null;
430 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
431 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
433 if (byteBuf != null) {
439 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
444 private String getCorrectUrlFormat(String longUrl) {
445 String temp = longUrl;
448 if (longUrl.isEmpty() || "ffmpeg".equals(longUrl)) {
453 url = new URL(longUrl);
454 int port = url.getPort();
456 if (url.getQuery() == null) {
457 temp = url.getPath();
459 temp = url.getPath() + "?" + url.getQuery();
462 if (url.getQuery() == null) {
463 temp = ":" + url.getPort() + url.getPath();
465 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
468 } catch (MalformedURLException e) {
469 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
474 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
475 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
476 sendHttpRequest("PUT", httpRequestURL, null);
479 public void sendHttpPOST(String httpPostURL, String content) {
480 FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, httpPostURL);
481 request.headers().set("Host", cameraConfig.getIp());
482 request.headers().add("Content-Type", "application/json");
483 request.headers().add("User-Agent",
484 "openHAB/" + FrameworkUtil.getBundle(this.getClass()).getVersion().toString());
485 request.headers().add("Accept", "*/*");
486 ByteBuf bbuf = Unpooled.copiedBuffer(content, StandardCharsets.UTF_8);
487 request.headers().set("Content-Length", bbuf.readableBytes());
488 request.content().clear().writeBytes(bbuf);
489 postRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
490 sendHttpRequest("POST", httpPostURL, null);
493 public void sendHttpPOST(String httpRequestURL) {
494 sendHttpRequest("POST", httpRequestURL, null);
497 public void sendHttpGET(String httpRequestURL) {
498 sendHttpRequest("GET", httpRequestURL, null);
501 public int getPortFromShortenedUrl(String httpRequestURL) {
502 if (httpRequestURL.startsWith(":")) {
503 int end = httpRequestURL.indexOf("/");
504 return Integer.parseInt(httpRequestURL.substring(1, end));
506 return cameraConfig.getPort();
509 public String getTinyUrl(String httpRequestURL) {
510 if (httpRequestURL.startsWith(":")) {
511 int beginIndex = httpRequestURL.indexOf("/");
512 return httpRequestURL.substring(beginIndex);
514 return httpRequestURL;
517 private void checkCameraConnection() {
518 if (snapshotPolling) {// Currently polling a real URL for snapshots, so camera must be online.
520 } else if (ffmpegSnapshotGeneration) {// Use RTSP stream creating snapshots to know camera is online.
521 Ffmpeg localSnapshot = ffmpegSnapshot;
522 if (localSnapshot != null && !localSnapshot.getIsAlive()) {
523 cameraCommunicationError("FFmpeg Snapshots Stopped: Check your camera can be reached.");
526 return;// ffmpeg snapshot stream is still alive
528 // Open a HTTP connection without sending any requests as we do not need a snapshot.
529 Bootstrap localBootstrap = mainBootstrap;
530 if (localBootstrap != null) {
531 ChannelFuture chFuture = localBootstrap
532 .connect(new InetSocketAddress(cameraConfig.getIp(), cameraConfig.getPort()));
533 if (chFuture.awaitUninterruptibly(500)) {
534 chFuture.channel().close();
538 cameraCommunicationError(
539 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
542 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
543 // The authHandler will generate a digest string and re-send using this same function when needed.
544 @SuppressWarnings("null")
545 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
546 int port = getPortFromShortenedUrl(httpRequestURLFull);
547 String httpRequestURL = getTinyUrl(httpRequestURLFull);
549 if (mainBootstrap == null) {
550 mainBootstrap = new Bootstrap();
551 mainBootstrap.group(mainEventLoopGroup);
552 mainBootstrap.channel(NioSocketChannel.class);
553 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
554 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
555 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
556 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
557 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
558 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
561 public void initChannel(SocketChannel socketChannel) throws Exception {
562 // HIK Alarm stream needs > 9sec idle to stop stream closing
563 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
564 socketChannel.pipeline().addLast(new HttpClientCodec());
565 socketChannel.pipeline().addLast(AUTH_HANDLER,
566 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
567 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
569 switch (thing.getThingTypeUID().getId()) {
571 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
574 socketChannel.pipeline()
575 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
578 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
581 socketChannel.pipeline().addLast(
582 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
584 case HIKVISION_THING:
585 socketChannel.pipeline()
586 .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
589 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
592 socketChannel.pipeline().addLast(REOLINK_HANDLER, new ReolinkHandler(getHandle()));
595 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
602 FullHttpRequest request;
603 if ("GET".equals(httpMethod) || (useDigestAuth && digestString == null)) {
604 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
605 request.headers().set("Host", cameraConfig.getIp() + ":" + port);
606 request.headers().set("Connection", HttpHeaderValues.CLOSE);
607 } else if ("PUT".equals(httpMethod)) {
608 request = putRequestWithBody;
610 request = postRequestWithBody;
613 if (!basicAuth.isEmpty()) {
615 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
618 request.headers().set("Authorization", "Basic " + basicAuth);
623 if (digestString != null) {
624 request.headers().set("Authorization", "Digest " + digestString);
628 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
629 .addListener(new ChannelFutureListener() {
632 public void operationComplete(@Nullable ChannelFuture future) {
633 if (future == null) {
636 if (future.isDone() && future.isSuccess()) {
637 Channel ch = future.channel();
638 openChannels.add(ch);
642 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
645 openChannel(ch, httpRequestURL);
646 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
647 commonHandler.setURL(httpRequestURLFull);
648 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
649 authHandler.setURL(httpMethod, httpRequestURL);
651 switch (thing.getThingTypeUID().getId()) {
653 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
654 amcrestHandler.setURL(httpRequestURL);
657 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
658 instarHandler.setURL(httpRequestURL);
661 ReolinkHandler reolinkHandler = (ReolinkHandler) ch.pipeline().get(REOLINK_HANDLER);
662 reolinkHandler.setURL(httpRequestURL);
665 ch.writeAndFlush(request);
666 } else { // an error occured
667 cameraCommunicationError(
668 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
674 public void processSnapshot(byte[] incommingSnapshot) {
675 lockCurrentSnapshot.lock();
677 currentSnapshot = incommingSnapshot;
678 if (cameraConfig.getGifPreroll() > 0) {
679 fifoSnapshotBuffer.add(incommingSnapshot);
680 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
681 fifoSnapshotBuffer.removeFirst();
685 lockCurrentSnapshot.unlock();
686 currentSnapshotTime = Instant.now();
689 if (updateImageChannel) {
690 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
691 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
692 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
693 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
694 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
695 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
696 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
700 public void startStreamServer() {
701 servlet = new CameraServlet(this, httpService);
702 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
703 + getThing().getUID().getId() + "/ipcamera.m3u8"));
704 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
705 + getThing().getUID().getId() + "/ipcamera.jpg"));
706 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
707 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
710 public void openCamerasStream() {
711 if (mjpegUri.isEmpty() || "ffmpeg".equals(mjpegUri)) {
712 setupFfmpegFormat(FFmpegFormat.MJPEG);
715 closeChannel(getTinyUrl(mjpegUri));
716 // Dahua cameras crash if you refresh (close and open) the stream without this delay.
717 mainEventLoopGroup.schedule(this::openMjpegStream, 300, TimeUnit.MILLISECONDS);
720 private void openMjpegStream() {
721 sendHttpGET(mjpegUri);
724 private void openChannel(Channel channel, String httpRequestURL) {
725 ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
726 if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
727 tracker.setChannel(channel);
730 channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
733 public void closeChannel(String url) {
734 ChannelTracking channelTracking = channelTrackingMap.get(url);
735 if (channelTracking != null) {
736 if (channelTracking.getChannel().isOpen()) {
737 channelTracking.getChannel().close();
744 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
745 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
748 @SuppressWarnings("PMD.CompareObjectsWithEquals")
749 private void cleanChannels() {
750 for (Channel channel : openChannels) {
751 boolean oldChannel = true;
752 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
753 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
754 channelTrackingMap.remove(channelTracking.getRequestUrl());
756 if (channelTracking.getChannel() == channel) {
757 logger.debug("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
767 public void storeHttpReply(String url, String content) {
768 ChannelTracking channelTracking = channelTrackingMap.get(url);
769 if (channelTracking != null) {
770 channelTracking.setReply(content);
774 private void storeSnapshots() {
776 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
777 lockCurrentSnapshot.lock();
779 for (byte[] foo : fifoSnapshotBuffer) {
780 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
783 OutputStream fos = new FileOutputStream(file);
786 } catch (FileNotFoundException e) {
787 logger.warn("FileNotFoundException {}", e.getMessage());
788 } catch (IOException e) {
789 logger.warn("IOException {}", e.getMessage());
793 lockCurrentSnapshot.unlock();
797 public void setupFfmpegFormat(FFmpegFormat format) {
798 String inputOptions = cameraConfig.getFfmpegInputOptions();
799 if (cameraConfig.getFfmpegOutput().isEmpty()) {
800 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
803 if (rtspUri.isEmpty()) {
804 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
807 if (cameraConfig.getFfmpegLocation().isEmpty()) {
808 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
811 if (rtspUri.toLowerCase().contains("rtsp")) {
812 if (inputOptions.isEmpty()) {
813 inputOptions = "-rtsp_transport tcp";
817 // Make sure the folder exists, if not create it.
818 new File(cameraConfig.getFfmpegOutput()).mkdirs();
821 if (ffmpegHLS == null) {
822 if (!inputOptions.isEmpty()) {
823 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
824 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
825 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
826 cameraConfig.getUser(), cameraConfig.getPassword());
828 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
829 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
830 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
831 cameraConfig.getPassword());
834 Ffmpeg localHLS = ffmpegHLS;
835 if (localHLS != null) {
836 localHLS.startConverting();
840 if (cameraConfig.getGifPreroll() > 0) {
841 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
842 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
843 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
844 + cameraConfig.getGifOutOptions(),
845 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
846 cameraConfig.getPassword());
848 if (!inputOptions.isEmpty()) {
849 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
851 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
853 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
854 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
855 cameraConfig.getUser(), cameraConfig.getPassword());
857 if (cameraConfig.getGifPreroll() > 0) {
860 Ffmpeg localGIF = ffmpegGIF;
861 if (localGIF != null) {
862 localGIF.startConverting();
863 if (gifHistory.isEmpty()) {
864 gifHistory = gifFilename;
865 } else if (!"ipcamera".equals(gifFilename)) {
866 gifHistory = gifFilename + "," + gifHistory;
867 if (gifHistoryLength > 49) {
868 int endIndex = gifHistory.lastIndexOf(",");
869 gifHistory = gifHistory.substring(0, endIndex);
872 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
876 if (!inputOptions.isEmpty()) {
877 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
879 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
881 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
882 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
883 cameraConfig.getUser(), cameraConfig.getPassword());
884 Ffmpeg localRecord = ffmpegRecord;
885 if (localRecord != null) {
886 localRecord.startConverting();
887 if (mp4History.isEmpty()) {
888 mp4History = mp4Filename;
889 } else if (!"ipcamera".equals(mp4Filename)) {
890 mp4History = mp4Filename + "," + mp4History;
891 if (mp4HistoryLength > 49) {
892 int endIndex = mp4History.lastIndexOf(",");
893 mp4History = mp4History.substring(0, endIndex);
897 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
900 Ffmpeg localAlarms = ffmpegRtspHelper;
901 if (localAlarms != null) {
902 localAlarms.stopConverting();
903 if (!ffmpegAudioAlarmEnabled && !ffmpegMotionAlarmEnabled) {
907 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
908 String filterOptions = "";
909 if (!ffmpegAudioAlarmEnabled) {
910 filterOptions = "-an";
912 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
914 if (!ffmpegMotionAlarmEnabled && !ffmpegSnapshotGeneration) {
915 filterOptions = filterOptions.concat(" -vn");
916 } else if (ffmpegMotionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
917 String usersMotionOptions = cameraConfig.getMotionOptions();
918 if (usersMotionOptions.startsWith("-")) {
919 // Need to put the users custom options first in the chain before the motion is detected
920 filterOptions += " " + usersMotionOptions + ",select='gte(scene,"
921 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
923 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
924 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
926 } else if (ffmpegMotionAlarmEnabled) {
927 filterOptions = filterOptions.concat(" -vf select='gte(scene,"
928 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print");
930 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
931 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
932 localAlarms = ffmpegRtspHelper;
933 if (localAlarms != null) {
934 localAlarms.startConverting();
938 if (ffmpegMjpeg == null) {
939 if (inputOptions.isEmpty()) {
940 inputOptions = "-hide_banner";
942 inputOptions += " -hide_banner";
944 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
945 cameraConfig.getMjpegOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
946 + getThing().getUID().getId() + "/ipcamera.jpg",
947 cameraConfig.getUser(), cameraConfig.getPassword());
949 Ffmpeg localMjpeg = ffmpegMjpeg;
950 if (localMjpeg != null) {
951 localMjpeg.startConverting();
955 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
956 if (ffmpegSnapshot == null) {
957 if (inputOptions.isEmpty()) {
959 inputOptions = "-threads 1 -skip_frame nokey -hide_banner";
961 inputOptions += " -threads 1 -skip_frame nokey -hide_banner";
963 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
964 cameraConfig.getSnapshotOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
965 + getThing().getUID().getId() + "/snapshot.jpg",
966 cameraConfig.getUser(), cameraConfig.getPassword());
968 Ffmpeg localSnaps = ffmpegSnapshot;
969 if (localSnaps != null) {
970 localSnaps.startConverting();
976 public void noMotionDetected(String thisAlarmsChannel) {
977 setChannelState(thisAlarmsChannel, OnOffType.OFF);
978 firstMotionAlarm = false;
979 motionAlarmUpdateSnapshot = false;
980 motionDetected = false;
981 if (streamingAutoFps) {
982 stopSnapshotPolling();
983 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
984 stopSnapshotPolling();
989 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
990 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
991 * tampering with the camera.
993 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
994 updateState(thisAlarmsChannel, state);
997 public void motionDetected(String thisAlarmsChannel) {
998 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
999 updateState(thisAlarmsChannel, OnOffType.ON);
1000 motionDetected = true;
1001 if (streamingAutoFps) {
1002 startSnapshotPolling();
1004 if (cameraConfig.getUpdateImageWhen().contains("2")) {
1005 if (!firstMotionAlarm) {
1006 if (!snapshotUri.isEmpty()) {
1009 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1011 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1012 if (!snapshotPolling) {
1013 startSnapshotPolling();
1015 firstMotionAlarm = true;
1016 motionAlarmUpdateSnapshot = true;
1020 public void audioDetected() {
1021 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1022 if (cameraConfig.getUpdateImageWhen().contains("3")) {
1023 if (!firstAudioAlarm) {
1024 if (!snapshotUri.isEmpty()) {
1027 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1029 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1030 firstAudioAlarm = true;
1031 audioAlarmUpdateSnapshot = true;
1035 public void noAudioDetected() {
1036 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1037 firstAudioAlarm = false;
1038 audioAlarmUpdateSnapshot = false;
1041 public void recordMp4(String filename, int seconds) {
1042 mp4Filename = filename;
1043 mp4RecordTime = seconds;
1044 setupFfmpegFormat(FFmpegFormat.RECORD);
1045 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1048 public void recordGif(String filename, int seconds) {
1049 gifFilename = filename;
1050 gifRecordTime = seconds;
1051 if (cameraConfig.getGifPreroll() > 0) {
1052 snapCount = seconds;
1054 setupFfmpegFormat(FFmpegFormat.GIF);
1056 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1059 private void getReolinkToken() {
1060 sendHttpPOST("/api.cgi?cmd=Login",
1061 "[{\"cmd\":\"Login\", \"param\":{ \"User\":{ \"Version\": \"0\", \"userName\":\""
1062 + cameraConfig.getUser() + "\", \"password\":\"" + cameraConfig.getPassword() + "\"}}}]");
1065 public String returnValueFromString(String rawString, String searchedString) {
1067 int index = rawString.indexOf(searchedString);
1068 if (index != -1) // -1 means "not found"
1070 result = rawString.substring(index + searchedString.length(), rawString.length());
1071 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1073 return result; // Did not find a carriage return.
1075 return result.substring(0, index);
1078 return ""; // Did not find the String we were searching for
1081 private void sendPTZRequest() {
1082 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1086 public void channelLinked(ChannelUID channelUID) {
1087 switch (channelUID.getId()) {
1088 case CHANNEL_MJPEG_URL:
1089 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1090 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
1092 case CHANNEL_HLS_URL:
1093 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1094 + getThing().getUID().getId() + "/ipcamera.m3u8"));
1096 case CHANNEL_IMAGE_URL:
1097 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1098 + getThing().getUID().getId() + "/ipcamera.jpg"));
1103 public void removeChannels(List<org.openhab.core.thing.Channel> removeChannels) {
1104 if (!removeChannels.isEmpty()) {
1105 ThingBuilder thingBuilder = editThing();
1106 thingBuilder.withoutChannels(removeChannels);
1107 updateThing(thingBuilder.build());
1112 public void handleCommand(ChannelUID channelUID, Command command) {
1113 if (command instanceof RefreshType) {
1114 switch (channelUID.getId()) {
1116 if (onvifCamera.supportsPTZ()) {
1117 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1121 if (onvifCamera.supportsPTZ()) {
1122 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1126 if (onvifCamera.supportsPTZ()) {
1127 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1130 case CHANNEL_GOTO_PRESET:
1131 if (onvifCamera.supportsPTZ()) {
1132 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1136 } // caution "REFRESH" can still progress to brand Handlers below the else.
1138 switch (channelUID.getId()) {
1139 case CHANNEL_MP4_HISTORY_LENGTH:
1140 if (DecimalType.ZERO.equals(command)) {
1141 mp4HistoryLength = 0;
1143 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1146 case CHANNEL_GIF_HISTORY_LENGTH:
1147 if (DecimalType.ZERO.equals(command)) {
1148 gifHistoryLength = 0;
1150 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1153 case CHANNEL_FFMPEG_MOTION_CONTROL:
1154 if (OnOffType.ON.equals(command)) {
1155 ffmpegMotionAlarmEnabled = true;
1156 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1157 ffmpegMotionAlarmEnabled = false;
1158 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1159 } else if (command instanceof PercentType) {
1160 ffmpegMotionAlarmEnabled = true;
1161 motionThreshold = ((PercentType) command).toBigDecimal();
1163 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1165 case CHANNEL_START_STREAM:
1167 if (OnOffType.ON.equals(command)) {
1168 localHLS = ffmpegHLS;
1169 if (localHLS == null) {
1170 setupFfmpegFormat(FFmpegFormat.HLS);
1171 localHLS = ffmpegHLS;
1173 if (localHLS != null) {
1174 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1175 localHLS.startConverting();
1178 localHLS = ffmpegHLS;
1179 if (localHLS != null) {
1180 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1181 localHLS.setKeepAlive(1);
1185 case CHANNEL_EXTERNAL_MOTION:
1186 if (OnOffType.ON.equals(command)) {
1187 motionDetected(CHANNEL_EXTERNAL_MOTION);
1189 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1192 case CHANNEL_GOTO_PRESET:
1193 if (onvifCamera.supportsPTZ()) {
1194 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1197 case CHANNEL_POLL_IMAGE:
1198 if (OnOffType.ON.equals(command)) {
1199 if (snapshotUri.isEmpty()) {
1200 ffmpegSnapshotGeneration = true;
1201 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1202 updateImageChannel = false;
1204 updateImageChannel = true;
1205 updateSnapshot();// Allows this to change Image FPS on demand
1208 Ffmpeg localSnaps = ffmpegSnapshot;
1209 if (localSnaps != null) {
1210 localSnaps.stopConverting();
1211 ffmpegSnapshotGeneration = false;
1213 updateImageChannel = false;
1217 if (onvifCamera.supportsPTZ()) {
1218 if (command instanceof IncreaseDecreaseType) {
1219 if (command == IncreaseDecreaseType.INCREASE) {
1220 if (cameraConfig.getPtzContinuous()) {
1221 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1223 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1226 if (cameraConfig.getPtzContinuous()) {
1227 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1229 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1233 } else if (OnOffType.OFF.equals(command)) {
1234 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1237 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1238 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1242 if (onvifCamera.supportsPTZ()) {
1243 if (command instanceof IncreaseDecreaseType) {
1244 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1245 if (cameraConfig.getPtzContinuous()) {
1246 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1248 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1251 if (cameraConfig.getPtzContinuous()) {
1252 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1254 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1258 } else if (OnOffType.OFF.equals(command)) {
1259 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1262 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1263 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1267 if (onvifCamera.supportsPTZ()) {
1268 if (command instanceof IncreaseDecreaseType) {
1269 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1270 if (cameraConfig.getPtzContinuous()) {
1271 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1273 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1276 if (cameraConfig.getPtzContinuous()) {
1277 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1279 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1283 } else if (OnOffType.OFF.equals(command)) {
1284 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1287 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1288 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1293 // commands and refresh now get passed to brand handlers
1294 switch (thing.getThingTypeUID().getId()) {
1296 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1297 amcrestHandler.handleCommand(channelUID, command);
1298 if (lowPriorityRequests.isEmpty()) {
1299 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1303 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1304 dahuaHandler.handleCommand(channelUID, command);
1305 if (lowPriorityRequests.isEmpty()) {
1306 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1309 case DOORBIRD_THING:
1310 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1311 doorBirdHandler.handleCommand(channelUID, command);
1312 if (lowPriorityRequests.isEmpty()) {
1313 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1316 case HIKVISION_THING:
1317 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1318 hikvisionHandler.handleCommand(channelUID, command);
1321 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1322 cameraConfig.getPassword());
1323 foscamHandler.handleCommand(channelUID, command);
1324 if (lowPriorityRequests.isEmpty()) {
1325 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1329 InstarHandler instarHandler = new InstarHandler(getHandle());
1330 instarHandler.handleCommand(channelUID, command);
1331 if (lowPriorityRequests.isEmpty()) {
1332 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1336 ReolinkHandler reolinkHandler = new ReolinkHandler(getHandle());
1337 reolinkHandler.handleCommand(channelUID, command);
1340 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1341 defaultHandler.handleCommand(channelUID, command);
1342 if (lowPriorityRequests.isEmpty()) {
1343 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1349 public void setChannelState(String channelToUpdate, State valueOf) {
1350 updateState(channelToUpdate, valueOf);
1353 private void bringCameraOnline() {
1355 updateStatus(ThingStatus.ONLINE);
1356 groupTracker.listOfOnlineCameraHandlers.add(this);
1357 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1358 Future<?> localFuture = cameraConnectionJob;
1359 if (localFuture != null) {
1360 localFuture.cancel(false);
1361 cameraConnectionJob = null;
1363 if (!snapshotUri.isEmpty()) {
1364 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1365 snapshotPolling = true;
1366 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000,
1367 cameraConfig.getPollTime(), TimeUnit.MILLISECONDS);
1371 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1373 // auto restart mjpeg stream now camera is back online.
1374 CameraServlet localServlet = servlet;
1375 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1376 openCamerasStream();
1379 if (!rtspUri.isEmpty()) {
1380 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1382 if (updateImageChannel) {
1383 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1385 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1387 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1388 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1389 handle.cameraOnline(getThing().getUID().getId());
1394 void snapshotIsFfmpeg() {
1395 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1397 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1398 bringCameraOnline();
1399 if (!rtspUri.isEmpty()) {
1400 updateImageChannel = false;
1401 ffmpegSnapshotGeneration = true;
1402 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1403 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1405 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1409 void pollingCameraConnection() {
1411 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)
1412 || thing.getThingTypeUID().getId().equals(DOORBIRD_THING)) {
1413 if (rtspUri.isEmpty()) {
1414 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1416 if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1419 ffmpegSnapshotGeneration = false;
1424 if (cameraConfig.getOnvifPort() > 0 && !onvifCamera.isConnected()) {
1425 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1426 cameraConfig.getOnvifPort());
1427 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1429 if ("ffmpeg".equals(snapshotUri)) {
1431 } else if (!snapshotUri.isEmpty()) {
1432 ffmpegSnapshotGeneration = false;
1434 } else if (!rtspUri.isEmpty()) {
1437 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1438 "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.");
1442 public void cameraConfigError(String reason) {
1443 // wont try to reconnect again due to a config error being the cause.
1444 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1448 public void cameraCommunicationError(String reason) {
1449 // will try to reconnect again as camera may be rebooting.
1450 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1451 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1452 resetAndRetryConnecting();
1456 private boolean streamIsStopped(String url) {
1457 ChannelTracking channelTracking = channelTrackingMap.get(url);
1458 if (channelTracking != null) {
1459 if (channelTracking.getChannel().isActive()) {
1460 return false; // stream is running.
1463 return true; // Stream stopped or never started.
1466 void snapshotRunnable() {
1467 // Snapshot should be first to keep consistent time between shots
1469 if (snapCount > 0) {
1470 if (--snapCount == 0) {
1471 setupFfmpegFormat(FFmpegFormat.GIF);
1476 private void takeSnapshot() {
1477 sendHttpGET(snapshotUri);
1480 private void updateSnapshot() {
1481 lastSnapshotRequest = Instant.now();
1482 mainEventLoopGroup.schedule(this::takeSnapshot, 0, TimeUnit.MILLISECONDS);
1485 public byte[] getSnapshot() {
1487 // Single gray pixel JPG to keep streams open when the camera goes offline so they dont stop.
1488 return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
1489 0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43,
1490 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
1491 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09,
1492 0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d,
1493 0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10,
1494 0x10, (byte) 0xff, (byte) 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
1495 (byte) 0xff, (byte) 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, (byte) 0xff, (byte) 0xda, 0x00, 0x08,
1496 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, (byte) 0xd2, (byte) 0xcf, 0x20, (byte) 0xff, (byte) 0xd9 };
1498 // Most cameras will return a 503 busy error if snapshot is faster than 1 second
1499 long lastUpdatedMs = Duration.between(lastSnapshotRequest, Instant.now()).toMillis();
1500 if (!snapshotPolling && !ffmpegSnapshotGeneration && lastUpdatedMs >= cameraConfig.getPollTime()) {
1503 lockCurrentSnapshot.lock();
1505 return currentSnapshot;
1507 lockCurrentSnapshot.unlock();
1511 public void stopSnapshotPolling() {
1512 Future<?> localFuture;
1513 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1514 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1515 snapshotPolling = false;
1516 localFuture = snapshotJob;
1517 if (localFuture != null) {
1518 localFuture.cancel(true);
1520 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1521 snapshotPolling = false;
1522 localFuture = snapshotJob;
1523 if (localFuture != null) {
1524 localFuture.cancel(true);
1529 public void startSnapshotPolling() {
1530 if (snapshotPolling || ffmpegSnapshotGeneration) {
1531 return; // Already polling or creating with FFmpeg from RTSP
1533 if (streamingSnapshotMjpeg || streamingAutoFps || cameraConfig.getUpdateImageWhen().contains("4")) {
1534 snapshotPolling = true;
1535 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 0, cameraConfig.getPollTime(),
1536 TimeUnit.MILLISECONDS);
1541 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep alarm
1542 * streams open and more.
1545 void pollCameraRunnable() {
1546 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1547 if (!lowPriorityRequests.isEmpty()) {
1548 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1549 lowPriorityCounter = 0;
1551 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1553 // what needs to be done every poll//
1554 switch (thing.getThingTypeUID().getId()) {
1556 if (!snapshotPolling) {
1557 checkCameraConnection();
1561 if (!snapshotPolling) {
1562 checkCameraConnection();
1564 if (!onvifCamera.isConnected()) {
1565 onvifCamera.connect(true);
1569 if (!snapshotPolling) {
1570 checkCameraConnection();
1572 noMotionDetected(CHANNEL_MOTION_ALARM);
1573 noMotionDetected(CHANNEL_PIR_ALARM);
1574 noMotionDetected(CHANNEL_HUMAN_ALARM);
1575 noMotionDetected(CHANNEL_CAR_ALARM);
1576 noMotionDetected(CHANNEL_ANIMAL_ALARM);
1579 case HIKVISION_THING:
1580 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1581 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1582 cameraConfig.getIp());
1583 sendHttpGET("/ISAPI/Event/notification/alertStream");
1587 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1588 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1591 if (cameraConfig.getNvrChannel() > 0) {
1592 sendHttpGET("/api.cgi?cmd=GetAiState&channel=" + cameraConfig.getNvrChannel() + "&user="
1593 + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword());
1594 sendHttpGET("/api.cgi?cmd=GetMdState&channel=" + cameraConfig.getNvrChannel() + "&user="
1595 + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword());
1597 if (!snapshotPolling) {
1598 checkCameraConnection();
1600 if (!onvifCamera.isConnected()) {
1601 onvifCamera.connect(true);
1606 if (!snapshotPolling) {
1607 checkCameraConnection();
1609 // Check for alarms, channel for NVRs appears not to work at filtering.
1610 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1611 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1612 cameraConfig.getIp());
1613 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1616 case DOORBIRD_THING:
1617 if (!snapshotPolling) {
1618 checkCameraConnection();
1620 // Check for alarms, channel for NVRs appears not to work at filtering.
1621 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1622 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1623 cameraConfig.getIp());
1624 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1628 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1629 + cameraConfig.getPassword());
1632 Ffmpeg localFfmpeg = ffmpegHLS;
1633 if (localFfmpeg != null) {
1634 localFfmpeg.checkKeepAlive();
1636 if (ffmpegMotionAlarmEnabled || ffmpegAudioAlarmEnabled) {
1637 localFfmpeg = ffmpegRtspHelper;
1638 if (localFfmpeg == null || !localFfmpeg.getIsAlive()) {
1639 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1642 // check if the thread has frozen due to camera doing a soft reboot
1643 localFfmpeg = ffmpegMjpeg;
1644 if (localFfmpeg != null && !localFfmpeg.getIsAlive()) {
1645 logger.debug("MJPEG was not being produced by FFmpeg when it should have been, restarting FFmpeg.");
1646 setupFfmpegFormat(FFmpegFormat.MJPEG);
1648 if (openChannels.size() > 10) {
1649 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1655 public void initialize() {
1656 cameraConfig = getConfigAs(CameraConfig.class);
1657 threadPool = Executors.newScheduledThreadPool(2);
1658 mainEventLoopGroup = new NioEventLoopGroup(3);
1659 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1660 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1661 rtspUri = cameraConfig.getFfmpegInput();
1662 if (cameraConfig.getFfmpegOutput().isEmpty()) {
1664 .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1666 // Known cameras will connect quicker if we skip ONVIF questions.
1667 switch (thing.getThingTypeUID().getId()) {
1670 if (mjpegUri.isEmpty()) {
1671 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1673 if (snapshotUri.isEmpty()) {
1674 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1677 case DOORBIRD_THING:
1678 if (mjpegUri.isEmpty()) {
1679 mjpegUri = "/bha-api/video.cgi";
1681 if (snapshotUri.isEmpty()) {
1682 snapshotUri = "/bha-api/image.cgi";
1686 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1687 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1688 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1689 if (mjpegUri.isEmpty()) {
1690 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1691 + cameraConfig.getPassword();
1693 if (snapshotUri.isEmpty()) {
1694 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1695 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1698 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1699 if (mjpegUri.isEmpty()) {
1700 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1702 if (snapshotUri.isEmpty()) {
1703 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1705 if (lowPriorityRequests.isEmpty()) {
1706 lowPriorityRequests.add("/ISAPI/System/IO/inputs/" + cameraConfig.getNvrChannel() + "/status");
1710 if (snapshotUri.isEmpty()) {
1711 snapshotUri = "/tmpfs/snap.jpg";
1713 if (mjpegUri.isEmpty()) {
1714 mjpegUri = "/mjpegstream.cgi?-chn=12";
1716 // Newer Instar cameras use this to setup the Alarm Server, plus it is used to work out which API is
1717 // implemented based on the response to these two requests.
1719 "/param.cgi?cmd=setasaction&-server=1&enable=1&-interval=1&cmd=setasattr&-as_index=1&-as_server="
1720 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1721 + getThing().getUID().getId()
1722 + "/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");
1723 // Older Instar cameras use this to setup the Alarm Server
1725 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
1726 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1727 + getThing().getUID().getId()
1728 + "/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");
1731 if (cameraConfig.useToken) {
1732 authenticationJob = threadPool.scheduleWithFixedDelay(this::getReolinkToken, 0, 45,
1735 reolinkAuth = "&user=" + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword();
1737 if (snapshotUri.isEmpty()) {
1738 if (cameraConfig.getNvrChannel() < 1) {
1739 snapshotUri = "/cgi-bin/api.cgi?cmd=Snap&channel=0&rs=openHAB" + reolinkAuth;
1741 snapshotUri = "/cgi-bin/api.cgi?cmd=Snap&channel=" + (cameraConfig.getNvrChannel() - 1)
1742 + "&rs=openHAB" + reolinkAuth;
1745 if (rtspUri.isEmpty()) {
1746 if (cameraConfig.getNvrChannel() < 1) {
1747 rtspUri = "rtsp://" + cameraConfig.getIp() + ":554/h264Preview_01_main";
1749 rtspUri = "rtsp://" + cameraConfig.getIp() + ":554/h264Preview_0" + cameraConfig.getNvrChannel()
1755 // for poll times 9 seconds and above don't display a warning about the Image channel.
1756 if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1758 "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.");
1760 // ONVIF and Instar event handling need the server started before connecting.
1761 startStreamServer();
1765 private void tryConnecting() {
1766 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)
1767 && !thing.getThingTypeUID().getId().equals(DOORBIRD_THING) && cameraConfig.getOnvifPort() > 0) {
1768 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1769 cameraConfig.getUser(), cameraConfig.getPassword());
1770 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1771 // Only use ONVIF events if it is not an API camera.
1772 onvifCamera.connect(supportsOnvifEvents());
1774 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 8, TimeUnit.SECONDS);
1777 private boolean supportsOnvifEvents() {
1778 switch (thing.getThingTypeUID().getId()) {
1782 if (cameraConfig.getNvrChannel() < 1) {
1789 private void keepMjpegRunning() {
1790 CameraServlet localServlet = servlet;
1791 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1792 if (!mjpegUri.isEmpty() && !"ffmpeg".equals(mjpegUri)) {
1793 localServlet.openStreams.queueFrame(("--" + localServlet.openStreams.boundary + "\r\n\r\n").getBytes());
1795 localServlet.openStreams.queueFrame(getSnapshot());
1799 // What the camera needs to re-connect if the initialize() is not called.
1800 private void resetAndRetryConnecting() {
1805 private void offline() {
1807 snapshotPolling = false;
1808 Future<?> localFuture = pollCameraJob;
1809 if (localFuture != null) {
1810 localFuture.cancel(true);
1811 pollCameraJob = null;
1813 localFuture = authenticationJob;
1814 if (localFuture != null) {
1815 localFuture.cancel(true);
1816 authenticationJob = null;
1818 localFuture = snapshotJob;
1819 if (localFuture != null) {
1820 localFuture.cancel(true);
1823 localFuture = cameraConnectionJob;
1824 if (localFuture != null) {
1825 localFuture.cancel(true);
1826 cameraConnectionJob = null;
1828 Ffmpeg localFfmpeg = ffmpegHLS;
1829 if (localFfmpeg != null) {
1830 localFfmpeg.stopConverting();
1833 localFfmpeg = ffmpegRecord;
1834 if (localFfmpeg != null) {
1835 localFfmpeg.stopConverting();
1836 ffmpegRecord = null;
1838 localFfmpeg = ffmpegGIF;
1839 if (localFfmpeg != null) {
1840 localFfmpeg.stopConverting();
1843 localFfmpeg = ffmpegRtspHelper;
1844 if (localFfmpeg != null) {
1845 localFfmpeg.stopConverting();
1846 ffmpegRtspHelper = null;
1848 localFfmpeg = ffmpegMjpeg;
1849 if (localFfmpeg != null) {
1850 localFfmpeg.stopConverting();
1853 localFfmpeg = ffmpegSnapshot;
1854 if (localFfmpeg != null) {
1855 localFfmpeg.stopConverting();
1856 ffmpegSnapshot = null;
1858 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {// generic cameras do not have ONVIF support
1859 onvifCamera.disconnect();
1861 openChannels.close();
1865 public void dispose() {
1867 CameraServlet localServlet = servlet;
1868 if (localServlet != null) {
1869 localServlet.dispose();
1872 threadPool.shutdown();
1873 // inform all group handlers that this camera has gone offline
1874 groupTracker.listOfOnlineCameraHandlers.remove(this);
1875 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1876 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1877 handle.cameraOffline(this);
1879 basicAuth = ""; // clear out stored Password hash
1880 useDigestAuth = false;
1881 mainEventLoopGroup.shutdownGracefully();
1882 mainBootstrap = null;
1883 channelTrackingMap.clear();
1886 public String getWhiteList() {
1887 return cameraConfig.getIpWhitelist();
1891 public Collection<Class<? extends ThingHandlerService>> getServices() {
1892 return Collections.singleton(IpCameraActions.class);