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 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
371 if (evt instanceof IdleStateEvent) {
372 IdleStateEvent e = (IdleStateEvent) evt;
373 // If camera does not use the channel for X amount of time it will close.
374 if (e.state() == IdleState.READER_IDLE) {
375 String urlToKeepOpen = "";
376 switch (thing.getThingTypeUID().getId()) {
378 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
381 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
384 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
385 if (channelTracking != null) {
386 if (channelTracking.getChannel().equals(ctx.channel())) {
387 return; // don't auto close this as it is for the alarms.
390 logger.debug("Closing an idle channel for camera:{}", cameraConfig.getIp());
397 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
398 IpCameraDynamicStateDescriptionProvider stateDescriptionProvider, HttpService httpService) {
400 this.stateDescriptionProvider = stateDescriptionProvider;
401 if (ipAddress != null) {
404 hostIp = Helper.getLocalIpAddress();
406 this.groupTracker = groupTracker;
407 this.httpService = httpService;
410 private IpCameraHandler getHandle() {
414 // false clears the stored user/pass hash, true creates the hash
415 public boolean setBasicAuth(boolean useBasic) {
417 logger.debug("Clearing out the stored BASIC auth now.");
420 } else if (!basicAuth.isEmpty()) {
421 // If the binding sends multiple requests before basicAuth was set, this may trigger falsely.
422 logger.warn("Camera is reporting your username and/or password is wrong.");
425 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
426 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
427 ByteBuf byteBuf = null;
429 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
430 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
432 if (byteBuf != null) {
438 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
443 private String getCorrectUrlFormat(String longUrl) {
444 String temp = longUrl;
447 if (longUrl.isEmpty() || "ffmpeg".equals(longUrl)) {
452 url = new URL(longUrl);
453 int port = url.getPort();
455 if (url.getQuery() == null) {
456 temp = url.getPath();
458 temp = url.getPath() + "?" + url.getQuery();
461 if (url.getQuery() == null) {
462 temp = ":" + url.getPort() + url.getPath();
464 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
467 } catch (MalformedURLException e) {
468 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
473 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
474 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
475 sendHttpRequest("PUT", httpRequestURL, null);
478 public void sendHttpPOST(String httpPostURL, String content) {
479 FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, httpPostURL);
480 request.headers().set("Host", cameraConfig.getIp());
481 request.headers().add("Content-Type", "application/json");
482 request.headers().add("User-Agent",
483 "openHAB/" + FrameworkUtil.getBundle(this.getClass()).getVersion().toString());
484 request.headers().add("Accept", "*/*");
485 ByteBuf bbuf = Unpooled.copiedBuffer(content, StandardCharsets.UTF_8);
486 request.headers().set("Content-Length", bbuf.readableBytes());
487 request.content().clear().writeBytes(bbuf);
488 postRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
489 sendHttpRequest("POST", httpPostURL, null);
492 public void sendHttpPOST(String httpRequestURL) {
493 sendHttpRequest("POST", httpRequestURL, null);
496 public void sendHttpGET(String httpRequestURL) {
497 sendHttpRequest("GET", httpRequestURL, null);
500 public int getPortFromShortenedUrl(String httpRequestURL) {
501 if (httpRequestURL.startsWith(":")) {
502 int end = httpRequestURL.indexOf("/");
503 return Integer.parseInt(httpRequestURL.substring(1, end));
505 return cameraConfig.getPort();
508 public String getTinyUrl(String httpRequestURL) {
509 if (httpRequestURL.startsWith(":")) {
510 int beginIndex = httpRequestURL.indexOf("/");
511 return httpRequestURL.substring(beginIndex);
513 return httpRequestURL;
516 private void checkCameraConnection() {
517 if (snapshotPolling) {// Currently polling a real URL for snapshots, so camera must be online.
519 } else if (ffmpegSnapshotGeneration) {// Use RTSP stream creating snapshots to know camera is online.
520 Ffmpeg localSnapshot = ffmpegSnapshot;
521 if (localSnapshot != null && !localSnapshot.getIsAlive()) {
522 cameraCommunicationError("FFmpeg Snapshots Stopped: Check your camera can be reached.");
525 return;// ffmpeg snapshot stream is still alive
527 // Open a HTTP connection without sending any requests as we do not need a snapshot.
528 Bootstrap localBootstrap = mainBootstrap;
529 if (localBootstrap != null) {
530 ChannelFuture chFuture = localBootstrap
531 .connect(new InetSocketAddress(cameraConfig.getIp(), cameraConfig.getPort()));
532 if (chFuture.awaitUninterruptibly(500)) {
533 chFuture.channel().close();
537 cameraCommunicationError(
538 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
541 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
542 // The authHandler will generate a digest string and re-send using this same function when needed.
543 @SuppressWarnings("null")
544 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
545 int port = getPortFromShortenedUrl(httpRequestURLFull);
546 String httpRequestURL = getTinyUrl(httpRequestURLFull);
548 if (mainBootstrap == null) {
549 mainBootstrap = new Bootstrap();
550 mainBootstrap.group(mainEventLoopGroup);
551 mainBootstrap.channel(NioSocketChannel.class);
552 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
553 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
554 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
555 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
556 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
557 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
560 public void initChannel(SocketChannel socketChannel) throws Exception {
561 // HIK Alarm stream needs > 9sec idle to stop stream closing
562 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
563 socketChannel.pipeline().addLast(new HttpClientCodec());
564 socketChannel.pipeline().addLast(AUTH_HANDLER,
565 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
566 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
568 switch (thing.getThingTypeUID().getId()) {
570 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
573 socketChannel.pipeline()
574 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
577 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
580 socketChannel.pipeline().addLast(
581 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
583 case HIKVISION_THING:
584 socketChannel.pipeline()
585 .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
588 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
591 socketChannel.pipeline().addLast(REOLINK_HANDLER, new ReolinkHandler(getHandle()));
594 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
601 FullHttpRequest request;
602 if ("GET".equals(httpMethod) || (useDigestAuth && digestString == null)) {
603 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
604 request.headers().set("Host", cameraConfig.getIp() + ":" + port);
605 request.headers().set("Connection", HttpHeaderValues.CLOSE);
606 } else if ("PUT".equals(httpMethod)) {
607 request = putRequestWithBody;
609 request = postRequestWithBody;
612 if (!basicAuth.isEmpty()) {
614 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
617 request.headers().set("Authorization", "Basic " + basicAuth);
622 if (digestString != null) {
623 request.headers().set("Authorization", "Digest " + digestString);
627 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
628 .addListener(new ChannelFutureListener() {
631 public void operationComplete(@Nullable ChannelFuture future) {
632 if (future == null) {
635 if (future.isDone() && future.isSuccess()) {
636 Channel ch = future.channel();
637 openChannels.add(ch);
641 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
644 openChannel(ch, httpRequestURL);
645 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
646 commonHandler.setURL(httpRequestURLFull);
647 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
648 authHandler.setURL(httpMethod, httpRequestURL);
650 switch (thing.getThingTypeUID().getId()) {
652 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
653 amcrestHandler.setURL(httpRequestURL);
656 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
657 instarHandler.setURL(httpRequestURL);
660 ReolinkHandler reolinkHandler = (ReolinkHandler) ch.pipeline().get(REOLINK_HANDLER);
661 reolinkHandler.setURL(httpRequestURL);
664 ch.writeAndFlush(request);
665 } else { // an error occured
666 cameraCommunicationError(
667 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
673 public void processSnapshot(byte[] incommingSnapshot) {
674 lockCurrentSnapshot.lock();
676 currentSnapshot = incommingSnapshot;
677 if (cameraConfig.getGifPreroll() > 0) {
678 fifoSnapshotBuffer.add(incommingSnapshot);
679 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
680 fifoSnapshotBuffer.removeFirst();
684 lockCurrentSnapshot.unlock();
685 currentSnapshotTime = Instant.now();
688 if (updateImageChannel) {
689 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
690 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
691 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
692 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
693 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
694 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
695 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
699 public void startStreamServer() {
700 servlet = new CameraServlet(this, httpService);
701 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
702 + getThing().getUID().getId() + "/ipcamera.m3u8"));
703 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
704 + getThing().getUID().getId() + "/ipcamera.jpg"));
705 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
706 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
709 public void openCamerasStream() {
710 if (mjpegUri.isEmpty() || "ffmpeg".equals(mjpegUri)) {
711 setupFfmpegFormat(FFmpegFormat.MJPEG);
714 closeChannel(getTinyUrl(mjpegUri));
715 // Dahua cameras crash if you refresh (close and open) the stream without this delay.
716 mainEventLoopGroup.schedule(this::openMjpegStream, 300, TimeUnit.MILLISECONDS);
719 private void openMjpegStream() {
720 sendHttpGET(mjpegUri);
723 private void openChannel(Channel channel, String httpRequestURL) {
724 ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
725 if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
726 tracker.setChannel(channel);
729 channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
732 public void closeChannel(String url) {
733 ChannelTracking channelTracking = channelTrackingMap.get(url);
734 if (channelTracking != null) {
735 if (channelTracking.getChannel().isOpen()) {
736 channelTracking.getChannel().close();
743 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
744 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
747 private void cleanChannels() {
748 for (Channel channel : openChannels) {
749 boolean oldChannel = true;
750 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
751 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
752 channelTrackingMap.remove(channelTracking.getRequestUrl());
754 if (channelTracking.getChannel().equals(channel)) {
755 logger.debug("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
765 public void storeHttpReply(String url, String content) {
766 ChannelTracking channelTracking = channelTrackingMap.get(url);
767 if (channelTracking != null) {
768 channelTracking.setReply(content);
772 private void storeSnapshots() {
774 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
775 lockCurrentSnapshot.lock();
777 for (byte[] foo : fifoSnapshotBuffer) {
778 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
781 OutputStream fos = new FileOutputStream(file);
784 } catch (FileNotFoundException e) {
785 logger.warn("FileNotFoundException {}", e.getMessage());
786 } catch (IOException e) {
787 logger.warn("IOException {}", e.getMessage());
791 lockCurrentSnapshot.unlock();
795 public void setupFfmpegFormat(FFmpegFormat format) {
796 String inputOptions = cameraConfig.getFfmpegInputOptions();
797 if (cameraConfig.getFfmpegOutput().isEmpty()) {
798 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
801 if (rtspUri.isEmpty()) {
802 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
805 if (cameraConfig.getFfmpegLocation().isEmpty()) {
806 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
809 if (rtspUri.toLowerCase().contains("rtsp")) {
810 if (inputOptions.isEmpty()) {
811 inputOptions = "-rtsp_transport tcp";
815 // Make sure the folder exists, if not create it.
816 new File(cameraConfig.getFfmpegOutput()).mkdirs();
819 if (ffmpegHLS == null) {
820 if (!inputOptions.isEmpty()) {
821 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
822 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
823 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
824 cameraConfig.getUser(), cameraConfig.getPassword());
826 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
827 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
828 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
829 cameraConfig.getPassword());
832 Ffmpeg localHLS = ffmpegHLS;
833 if (localHLS != null) {
834 localHLS.startConverting();
838 if (cameraConfig.getGifPreroll() > 0) {
839 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
840 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
841 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
842 + cameraConfig.getGifOutOptions(),
843 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
844 cameraConfig.getPassword());
846 if (!inputOptions.isEmpty()) {
847 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
849 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
851 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
852 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
853 cameraConfig.getUser(), cameraConfig.getPassword());
855 if (cameraConfig.getGifPreroll() > 0) {
858 Ffmpeg localGIF = ffmpegGIF;
859 if (localGIF != null) {
860 localGIF.startConverting();
861 if (gifHistory.isEmpty()) {
862 gifHistory = gifFilename;
863 } else if (!"ipcamera".equals(gifFilename)) {
864 gifHistory = gifFilename + "," + gifHistory;
865 if (gifHistoryLength > 49) {
866 int endIndex = gifHistory.lastIndexOf(",");
867 gifHistory = gifHistory.substring(0, endIndex);
870 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
874 if (!inputOptions.isEmpty()) {
875 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
877 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
879 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
880 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
881 cameraConfig.getUser(), cameraConfig.getPassword());
882 Ffmpeg localRecord = ffmpegRecord;
883 if (localRecord != null) {
884 localRecord.startConverting();
885 if (mp4History.isEmpty()) {
886 mp4History = mp4Filename;
887 } else if (!"ipcamera".equals(mp4Filename)) {
888 mp4History = mp4Filename + "," + mp4History;
889 if (mp4HistoryLength > 49) {
890 int endIndex = mp4History.lastIndexOf(",");
891 mp4History = mp4History.substring(0, endIndex);
895 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
898 Ffmpeg localAlarms = ffmpegRtspHelper;
899 if (localAlarms != null) {
900 localAlarms.stopConverting();
901 if (!ffmpegAudioAlarmEnabled && !ffmpegMotionAlarmEnabled) {
905 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
906 String filterOptions = "";
907 if (!ffmpegAudioAlarmEnabled) {
908 filterOptions = "-an";
910 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
912 if (!ffmpegMotionAlarmEnabled && !ffmpegSnapshotGeneration) {
913 filterOptions = filterOptions.concat(" -vn");
914 } else if (ffmpegMotionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
915 String usersMotionOptions = cameraConfig.getMotionOptions();
916 if (usersMotionOptions.startsWith("-")) {
917 // Need to put the users custom options first in the chain before the motion is detected
918 filterOptions += " " + usersMotionOptions + ",select='gte(scene,"
919 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
921 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
922 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
924 } else if (ffmpegMotionAlarmEnabled) {
925 filterOptions = filterOptions.concat(" -vf select='gte(scene,"
926 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print");
928 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
929 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
930 localAlarms = ffmpegRtspHelper;
931 if (localAlarms != null) {
932 localAlarms.startConverting();
936 if (ffmpegMjpeg == null) {
937 if (inputOptions.isEmpty()) {
938 inputOptions = "-hide_banner";
940 inputOptions += " -hide_banner";
942 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
943 cameraConfig.getMjpegOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
944 + getThing().getUID().getId() + "/ipcamera.jpg",
945 cameraConfig.getUser(), cameraConfig.getPassword());
947 Ffmpeg localMjpeg = ffmpegMjpeg;
948 if (localMjpeg != null) {
949 localMjpeg.startConverting();
953 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
954 if (ffmpegSnapshot == null) {
955 if (inputOptions.isEmpty()) {
957 inputOptions = "-threads 1 -skip_frame nokey -hide_banner";
959 inputOptions += " -threads 1 -skip_frame nokey -hide_banner";
961 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
962 cameraConfig.getSnapshotOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
963 + getThing().getUID().getId() + "/snapshot.jpg",
964 cameraConfig.getUser(), cameraConfig.getPassword());
966 Ffmpeg localSnaps = ffmpegSnapshot;
967 if (localSnaps != null) {
968 localSnaps.startConverting();
974 public void noMotionDetected(String thisAlarmsChannel) {
975 setChannelState(thisAlarmsChannel, OnOffType.OFF);
976 firstMotionAlarm = false;
977 motionAlarmUpdateSnapshot = false;
978 motionDetected = false;
979 if (streamingAutoFps) {
980 stopSnapshotPolling();
981 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
982 stopSnapshotPolling();
987 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
988 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
989 * tampering with the camera.
991 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
992 updateState(thisAlarmsChannel, state);
995 public void motionDetected(String thisAlarmsChannel) {
996 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
997 updateState(thisAlarmsChannel, OnOffType.ON);
998 motionDetected = true;
999 if (streamingAutoFps) {
1000 startSnapshotPolling();
1002 if (cameraConfig.getUpdateImageWhen().contains("2")) {
1003 if (!firstMotionAlarm) {
1004 if (!snapshotUri.isEmpty()) {
1007 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1009 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1010 if (!snapshotPolling) {
1011 startSnapshotPolling();
1013 firstMotionAlarm = true;
1014 motionAlarmUpdateSnapshot = true;
1018 public void audioDetected() {
1019 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1020 if (cameraConfig.getUpdateImageWhen().contains("3")) {
1021 if (!firstAudioAlarm) {
1022 if (!snapshotUri.isEmpty()) {
1025 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1027 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1028 firstAudioAlarm = true;
1029 audioAlarmUpdateSnapshot = true;
1033 public void noAudioDetected() {
1034 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1035 firstAudioAlarm = false;
1036 audioAlarmUpdateSnapshot = false;
1039 public void recordMp4(String filename, int seconds) {
1040 mp4Filename = filename;
1041 mp4RecordTime = seconds;
1042 setupFfmpegFormat(FFmpegFormat.RECORD);
1043 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1046 public void recordGif(String filename, int seconds) {
1047 gifFilename = filename;
1048 gifRecordTime = seconds;
1049 if (cameraConfig.getGifPreroll() > 0) {
1050 snapCount = seconds;
1052 setupFfmpegFormat(FFmpegFormat.GIF);
1054 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1057 private void getReolinkToken() {
1058 sendHttpPOST("/api.cgi?cmd=Login",
1059 "[{\"cmd\":\"Login\", \"param\":{ \"User\":{ \"Version\": \"0\", \"userName\":\""
1060 + cameraConfig.getUser() + "\", \"password\":\"" + cameraConfig.getPassword() + "\"}}}]");
1063 public String returnValueFromString(String rawString, String searchedString) {
1065 int index = rawString.indexOf(searchedString);
1066 if (index != -1) // -1 means "not found"
1068 result = rawString.substring(index + searchedString.length(), rawString.length());
1069 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1071 return result; // Did not find a carriage return.
1073 return result.substring(0, index);
1076 return ""; // Did not find the String we were searching for
1079 private void sendPTZRequest() {
1080 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1084 public void channelLinked(ChannelUID channelUID) {
1085 switch (channelUID.getId()) {
1086 case CHANNEL_MJPEG_URL:
1087 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1088 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
1090 case CHANNEL_HLS_URL:
1091 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1092 + getThing().getUID().getId() + "/ipcamera.m3u8"));
1094 case CHANNEL_IMAGE_URL:
1095 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1096 + getThing().getUID().getId() + "/ipcamera.jpg"));
1101 public void removeChannels(List<org.openhab.core.thing.Channel> removeChannels) {
1102 if (!removeChannels.isEmpty()) {
1103 ThingBuilder thingBuilder = editThing();
1104 thingBuilder.withoutChannels(removeChannels);
1105 updateThing(thingBuilder.build());
1110 public void handleCommand(ChannelUID channelUID, Command command) {
1111 if (command instanceof RefreshType) {
1112 switch (channelUID.getId()) {
1114 if (onvifCamera.supportsPTZ()) {
1115 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1119 if (onvifCamera.supportsPTZ()) {
1120 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1124 if (onvifCamera.supportsPTZ()) {
1125 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1128 case CHANNEL_GOTO_PRESET:
1129 if (onvifCamera.supportsPTZ()) {
1130 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1134 } // caution "REFRESH" can still progress to brand Handlers below the else.
1136 switch (channelUID.getId()) {
1137 case CHANNEL_MP4_HISTORY_LENGTH:
1138 if (DecimalType.ZERO.equals(command)) {
1139 mp4HistoryLength = 0;
1141 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1144 case CHANNEL_GIF_HISTORY_LENGTH:
1145 if (DecimalType.ZERO.equals(command)) {
1146 gifHistoryLength = 0;
1148 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1151 case CHANNEL_FFMPEG_MOTION_CONTROL:
1152 if (OnOffType.ON.equals(command)) {
1153 ffmpegMotionAlarmEnabled = true;
1154 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1155 ffmpegMotionAlarmEnabled = false;
1156 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1157 } else if (command instanceof PercentType) {
1158 ffmpegMotionAlarmEnabled = true;
1159 motionThreshold = ((PercentType) command).toBigDecimal();
1161 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1163 case CHANNEL_START_STREAM:
1165 if (OnOffType.ON.equals(command)) {
1166 localHLS = ffmpegHLS;
1167 if (localHLS == null) {
1168 setupFfmpegFormat(FFmpegFormat.HLS);
1169 localHLS = ffmpegHLS;
1171 if (localHLS != null) {
1172 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1173 localHLS.startConverting();
1176 localHLS = ffmpegHLS;
1177 if (localHLS != null) {
1178 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1179 localHLS.setKeepAlive(1);
1183 case CHANNEL_EXTERNAL_MOTION:
1184 if (OnOffType.ON.equals(command)) {
1185 motionDetected(CHANNEL_EXTERNAL_MOTION);
1187 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1190 case CHANNEL_GOTO_PRESET:
1191 if (onvifCamera.supportsPTZ()) {
1192 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1195 case CHANNEL_POLL_IMAGE:
1196 if (OnOffType.ON.equals(command)) {
1197 if (snapshotUri.isEmpty()) {
1198 ffmpegSnapshotGeneration = true;
1199 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1200 updateImageChannel = false;
1202 updateImageChannel = true;
1203 updateSnapshot();// Allows this to change Image FPS on demand
1206 Ffmpeg localSnaps = ffmpegSnapshot;
1207 if (localSnaps != null) {
1208 localSnaps.stopConverting();
1209 ffmpegSnapshotGeneration = false;
1211 updateImageChannel = false;
1215 if (onvifCamera.supportsPTZ()) {
1216 if (command instanceof IncreaseDecreaseType) {
1217 if (command == IncreaseDecreaseType.INCREASE) {
1218 if (cameraConfig.getPtzContinuous()) {
1219 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1221 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1224 if (cameraConfig.getPtzContinuous()) {
1225 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1227 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1231 } else if (OnOffType.OFF.equals(command)) {
1232 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1235 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1236 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1240 if (onvifCamera.supportsPTZ()) {
1241 if (command instanceof IncreaseDecreaseType) {
1242 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1243 if (cameraConfig.getPtzContinuous()) {
1244 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1246 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1249 if (cameraConfig.getPtzContinuous()) {
1250 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1252 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1256 } else if (OnOffType.OFF.equals(command)) {
1257 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1260 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1261 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1265 if (onvifCamera.supportsPTZ()) {
1266 if (command instanceof IncreaseDecreaseType) {
1267 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1268 if (cameraConfig.getPtzContinuous()) {
1269 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1271 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1274 if (cameraConfig.getPtzContinuous()) {
1275 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1277 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1281 } else if (OnOffType.OFF.equals(command)) {
1282 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1285 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1286 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1291 // commands and refresh now get passed to brand handlers
1292 switch (thing.getThingTypeUID().getId()) {
1294 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1295 amcrestHandler.handleCommand(channelUID, command);
1296 if (lowPriorityRequests.isEmpty()) {
1297 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1301 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1302 dahuaHandler.handleCommand(channelUID, command);
1303 if (lowPriorityRequests.isEmpty()) {
1304 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1307 case DOORBIRD_THING:
1308 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1309 doorBirdHandler.handleCommand(channelUID, command);
1310 if (lowPriorityRequests.isEmpty()) {
1311 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1314 case HIKVISION_THING:
1315 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1316 hikvisionHandler.handleCommand(channelUID, command);
1319 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1320 cameraConfig.getPassword());
1321 foscamHandler.handleCommand(channelUID, command);
1322 if (lowPriorityRequests.isEmpty()) {
1323 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1327 InstarHandler instarHandler = new InstarHandler(getHandle());
1328 instarHandler.handleCommand(channelUID, command);
1329 if (lowPriorityRequests.isEmpty()) {
1330 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1334 ReolinkHandler reolinkHandler = new ReolinkHandler(getHandle());
1335 reolinkHandler.handleCommand(channelUID, command);
1338 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1339 defaultHandler.handleCommand(channelUID, command);
1340 if (lowPriorityRequests.isEmpty()) {
1341 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1347 public void setChannelState(String channelToUpdate, State valueOf) {
1348 updateState(channelToUpdate, valueOf);
1351 private void bringCameraOnline() {
1353 updateStatus(ThingStatus.ONLINE);
1354 groupTracker.listOfOnlineCameraHandlers.add(this);
1355 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1356 Future<?> localFuture = cameraConnectionJob;
1357 if (localFuture != null) {
1358 localFuture.cancel(false);
1359 cameraConnectionJob = null;
1361 if (!snapshotUri.isEmpty()) {
1362 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1363 snapshotPolling = true;
1364 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000,
1365 cameraConfig.getPollTime(), TimeUnit.MILLISECONDS);
1369 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1371 // auto restart mjpeg stream now camera is back online.
1372 CameraServlet localServlet = servlet;
1373 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1374 openCamerasStream();
1377 if (!rtspUri.isEmpty()) {
1378 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1380 if (updateImageChannel) {
1381 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1383 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1385 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1386 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1387 handle.cameraOnline(getThing().getUID().getId());
1392 void snapshotIsFfmpeg() {
1393 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1395 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1396 bringCameraOnline();
1397 if (!rtspUri.isEmpty()) {
1398 updateImageChannel = false;
1399 ffmpegSnapshotGeneration = true;
1400 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1401 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1403 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1407 void pollingCameraConnection() {
1409 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)
1410 || thing.getThingTypeUID().getId().equals(DOORBIRD_THING)) {
1411 if (rtspUri.isEmpty()) {
1412 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1414 if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1417 ffmpegSnapshotGeneration = false;
1422 if (cameraConfig.getOnvifPort() > 0 && !onvifCamera.isConnected()) {
1423 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1424 cameraConfig.getOnvifPort());
1425 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1427 if ("ffmpeg".equals(snapshotUri)) {
1429 } else if (!snapshotUri.isEmpty()) {
1430 ffmpegSnapshotGeneration = false;
1432 } else if (!rtspUri.isEmpty()) {
1435 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1436 "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.");
1440 public void cameraConfigError(String reason) {
1441 // wont try to reconnect again due to a config error being the cause.
1442 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1446 public void cameraCommunicationError(String reason) {
1447 // will try to reconnect again as camera may be rebooting.
1448 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1449 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1450 resetAndRetryConnecting();
1454 private boolean streamIsStopped(String url) {
1455 ChannelTracking channelTracking = channelTrackingMap.get(url);
1456 if (channelTracking != null) {
1457 if (channelTracking.getChannel().isActive()) {
1458 return false; // stream is running.
1461 return true; // Stream stopped or never started.
1464 void snapshotRunnable() {
1465 // Snapshot should be first to keep consistent time between shots
1467 if (snapCount > 0) {
1468 if (--snapCount == 0) {
1469 setupFfmpegFormat(FFmpegFormat.GIF);
1474 private void takeSnapshot() {
1475 sendHttpGET(snapshotUri);
1478 private void updateSnapshot() {
1479 lastSnapshotRequest = Instant.now();
1480 mainEventLoopGroup.schedule(this::takeSnapshot, 0, TimeUnit.MILLISECONDS);
1483 public byte[] getSnapshot() {
1485 // Single gray pixel JPG to keep streams open when the camera goes offline so they dont stop.
1486 return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
1487 0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43,
1488 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
1489 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09,
1490 0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d,
1491 0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10,
1492 0x10, (byte) 0xff, (byte) 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
1493 (byte) 0xff, (byte) 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, (byte) 0xff, (byte) 0xda, 0x00, 0x08,
1494 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, (byte) 0xd2, (byte) 0xcf, 0x20, (byte) 0xff, (byte) 0xd9 };
1496 // Most cameras will return a 503 busy error if snapshot is faster than 1 second
1497 long lastUpdatedMs = Duration.between(lastSnapshotRequest, Instant.now()).toMillis();
1498 if (!snapshotPolling && !ffmpegSnapshotGeneration && lastUpdatedMs >= cameraConfig.getPollTime()) {
1501 lockCurrentSnapshot.lock();
1503 return currentSnapshot;
1505 lockCurrentSnapshot.unlock();
1509 public void stopSnapshotPolling() {
1510 Future<?> localFuture;
1511 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1512 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1513 snapshotPolling = false;
1514 localFuture = snapshotJob;
1515 if (localFuture != null) {
1516 localFuture.cancel(true);
1518 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1519 snapshotPolling = false;
1520 localFuture = snapshotJob;
1521 if (localFuture != null) {
1522 localFuture.cancel(true);
1527 public void startSnapshotPolling() {
1528 if (snapshotPolling || ffmpegSnapshotGeneration) {
1529 return; // Already polling or creating with FFmpeg from RTSP
1531 if (streamingSnapshotMjpeg || streamingAutoFps || cameraConfig.getUpdateImageWhen().contains("4")) {
1532 snapshotPolling = true;
1533 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 0, cameraConfig.getPollTime(),
1534 TimeUnit.MILLISECONDS);
1539 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep alarm
1540 * streams open and more.
1543 void pollCameraRunnable() {
1544 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1545 if (!lowPriorityRequests.isEmpty()) {
1546 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1547 lowPriorityCounter = 0;
1549 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1551 // what needs to be done every poll//
1552 switch (thing.getThingTypeUID().getId()) {
1554 if (!snapshotPolling) {
1555 checkCameraConnection();
1559 if (!snapshotPolling) {
1560 checkCameraConnection();
1562 if (!onvifCamera.isConnected()) {
1563 onvifCamera.connect(true);
1567 if (!snapshotPolling) {
1568 checkCameraConnection();
1570 noMotionDetected(CHANNEL_MOTION_ALARM);
1571 noMotionDetected(CHANNEL_PIR_ALARM);
1572 noMotionDetected(CHANNEL_HUMAN_ALARM);
1573 noMotionDetected(CHANNEL_CAR_ALARM);
1574 noMotionDetected(CHANNEL_ANIMAL_ALARM);
1577 case HIKVISION_THING:
1578 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1579 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1580 cameraConfig.getIp());
1581 sendHttpGET("/ISAPI/Event/notification/alertStream");
1585 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1586 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1589 if (cameraConfig.getNvrChannel() > 0) {
1590 sendHttpGET("/api.cgi?cmd=GetAiState&channel=" + cameraConfig.getNvrChannel() + "&user="
1591 + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword());
1592 sendHttpGET("/api.cgi?cmd=GetMdState&channel=" + cameraConfig.getNvrChannel() + "&user="
1593 + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword());
1595 if (!snapshotPolling) {
1596 checkCameraConnection();
1598 if (!onvifCamera.isConnected()) {
1599 onvifCamera.connect(true);
1604 if (!snapshotPolling) {
1605 checkCameraConnection();
1607 // Check for alarms, channel for NVRs appears not to work at filtering.
1608 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1609 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1610 cameraConfig.getIp());
1611 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1614 case DOORBIRD_THING:
1615 if (!snapshotPolling) {
1616 checkCameraConnection();
1618 // Check for alarms, channel for NVRs appears not to work at filtering.
1619 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1620 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1621 cameraConfig.getIp());
1622 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1626 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1627 + cameraConfig.getPassword());
1630 Ffmpeg localFfmpeg = ffmpegHLS;
1631 if (localFfmpeg != null) {
1632 localFfmpeg.checkKeepAlive();
1634 if (ffmpegMotionAlarmEnabled || ffmpegAudioAlarmEnabled) {
1635 localFfmpeg = ffmpegRtspHelper;
1636 if (localFfmpeg == null || !localFfmpeg.getIsAlive()) {
1637 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1640 // check if the thread has frozen due to camera doing a soft reboot
1641 localFfmpeg = ffmpegMjpeg;
1642 if (localFfmpeg != null && !localFfmpeg.getIsAlive()) {
1643 logger.debug("MJPEG was not being produced by FFmpeg when it should have been, restarting FFmpeg.");
1644 setupFfmpegFormat(FFmpegFormat.MJPEG);
1646 if (openChannels.size() > 10) {
1647 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1653 public void initialize() {
1654 cameraConfig = getConfigAs(CameraConfig.class);
1655 threadPool = Executors.newScheduledThreadPool(2);
1656 mainEventLoopGroup = new NioEventLoopGroup(3);
1657 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1658 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1659 rtspUri = cameraConfig.getFfmpegInput();
1660 if (cameraConfig.getFfmpegOutput().isEmpty()) {
1662 .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1664 // Known cameras will connect quicker if we skip ONVIF questions.
1665 switch (thing.getThingTypeUID().getId()) {
1668 if (mjpegUri.isEmpty()) {
1669 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1671 if (snapshotUri.isEmpty()) {
1672 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1675 case DOORBIRD_THING:
1676 if (mjpegUri.isEmpty()) {
1677 mjpegUri = "/bha-api/video.cgi";
1679 if (snapshotUri.isEmpty()) {
1680 snapshotUri = "/bha-api/image.cgi";
1684 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1685 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1686 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1687 if (mjpegUri.isEmpty()) {
1688 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1689 + cameraConfig.getPassword();
1691 if (snapshotUri.isEmpty()) {
1692 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1693 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1696 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1697 if (mjpegUri.isEmpty()) {
1698 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1700 if (snapshotUri.isEmpty()) {
1701 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1703 if (lowPriorityRequests.isEmpty()) {
1704 lowPriorityRequests.add("/ISAPI/System/IO/inputs/" + cameraConfig.getNvrChannel() + "/status");
1708 if (snapshotUri.isEmpty()) {
1709 snapshotUri = "/tmpfs/snap.jpg";
1711 if (mjpegUri.isEmpty()) {
1712 mjpegUri = "/mjpegstream.cgi?-chn=12";
1714 // Newer Instar cameras use this to setup the Alarm Server, plus it is used to work out which API is
1715 // implemented based on the response to these two requests.
1717 "/param.cgi?cmd=setasaction&-server=1&enable=1&-interval=1&cmd=setasattr&-as_index=1&-as_server="
1718 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1719 + getThing().getUID().getId()
1720 + "/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");
1721 // Older Instar cameras use this to setup the Alarm Server
1723 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
1724 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1725 + getThing().getUID().getId()
1726 + "/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");
1729 if (cameraConfig.useToken) {
1730 authenticationJob = threadPool.scheduleWithFixedDelay(this::getReolinkToken, 0, 45,
1733 reolinkAuth = "&user=" + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword();
1735 if (snapshotUri.isEmpty()) {
1736 if (cameraConfig.getNvrChannel() < 1) {
1737 snapshotUri = "/cgi-bin/api.cgi?cmd=Snap&channel=0&rs=openHAB" + reolinkAuth;
1739 snapshotUri = "/cgi-bin/api.cgi?cmd=Snap&channel=" + (cameraConfig.getNvrChannel() - 1)
1740 + "&rs=openHAB" + reolinkAuth;
1743 if (rtspUri.isEmpty()) {
1744 if (cameraConfig.getNvrChannel() < 1) {
1745 rtspUri = "rtsp://" + cameraConfig.getIp() + ":554/h264Preview_01_main";
1747 rtspUri = "rtsp://" + cameraConfig.getIp() + ":554/h264Preview_0" + cameraConfig.getNvrChannel()
1753 // for poll times 9 seconds and above don't display a warning about the Image channel.
1754 if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1756 "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.");
1758 // ONVIF and Instar event handling need the server started before connecting.
1759 startStreamServer();
1763 private void tryConnecting() {
1764 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)
1765 && !thing.getThingTypeUID().getId().equals(DOORBIRD_THING) && cameraConfig.getOnvifPort() > 0) {
1766 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1767 cameraConfig.getUser(), cameraConfig.getPassword());
1768 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1769 // Only use ONVIF events if it is not an API camera.
1770 onvifCamera.connect(supportsOnvifEvents());
1772 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 8, TimeUnit.SECONDS);
1775 private boolean supportsOnvifEvents() {
1776 switch (thing.getThingTypeUID().getId()) {
1780 if (cameraConfig.getNvrChannel() < 1) {
1787 private void keepMjpegRunning() {
1788 CameraServlet localServlet = servlet;
1789 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1790 if (!mjpegUri.isEmpty() && !"ffmpeg".equals(mjpegUri)) {
1791 localServlet.openStreams.queueFrame(("--" + localServlet.openStreams.boundary + "\r\n\r\n").getBytes());
1793 localServlet.openStreams.queueFrame(getSnapshot());
1797 // What the camera needs to re-connect if the initialize() is not called.
1798 private void resetAndRetryConnecting() {
1803 private void offline() {
1805 snapshotPolling = false;
1806 Future<?> localFuture = pollCameraJob;
1807 if (localFuture != null) {
1808 localFuture.cancel(true);
1809 pollCameraJob = null;
1811 localFuture = authenticationJob;
1812 if (localFuture != null) {
1813 localFuture.cancel(true);
1814 authenticationJob = null;
1816 localFuture = snapshotJob;
1817 if (localFuture != null) {
1818 localFuture.cancel(true);
1821 localFuture = cameraConnectionJob;
1822 if (localFuture != null) {
1823 localFuture.cancel(true);
1824 cameraConnectionJob = null;
1826 Ffmpeg localFfmpeg = ffmpegHLS;
1827 if (localFfmpeg != null) {
1828 localFfmpeg.stopConverting();
1831 localFfmpeg = ffmpegRecord;
1832 if (localFfmpeg != null) {
1833 localFfmpeg.stopConverting();
1834 ffmpegRecord = null;
1836 localFfmpeg = ffmpegGIF;
1837 if (localFfmpeg != null) {
1838 localFfmpeg.stopConverting();
1841 localFfmpeg = ffmpegRtspHelper;
1842 if (localFfmpeg != null) {
1843 localFfmpeg.stopConverting();
1844 ffmpegRtspHelper = null;
1846 localFfmpeg = ffmpegMjpeg;
1847 if (localFfmpeg != null) {
1848 localFfmpeg.stopConverting();
1851 localFfmpeg = ffmpegSnapshot;
1852 if (localFfmpeg != null) {
1853 localFfmpeg.stopConverting();
1854 ffmpegSnapshot = null;
1856 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {// generic cameras do not have ONVIF support
1857 onvifCamera.disconnect();
1859 openChannels.close();
1863 public void dispose() {
1865 CameraServlet localServlet = servlet;
1866 if (localServlet != null) {
1867 localServlet.dispose();
1870 threadPool.shutdown();
1871 // inform all group handlers that this camera has gone offline
1872 groupTracker.listOfOnlineCameraHandlers.remove(this);
1873 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1874 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1875 handle.cameraOffline(this);
1877 basicAuth = ""; // clear out stored Password hash
1878 useDigestAuth = false;
1879 mainEventLoopGroup.shutdownGracefully();
1880 mainBootstrap = null;
1881 channelTrackingMap.clear();
1884 public String getWhiteList() {
1885 return cameraConfig.getIpWhitelist();
1889 public Collection<Class<? extends ThingHandlerService>> getServices() {
1890 return Collections.singleton(IpCameraActions.class);