2 * Copyright (c) 2010-2022 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.time.Duration;
27 import java.time.Instant;
28 import java.util.ArrayList;
29 import java.util.Collection;
30 import java.util.Collections;
31 import java.util.LinkedList;
32 import java.util.List;
34 import java.util.concurrent.ConcurrentHashMap;
35 import java.util.concurrent.Executors;
36 import java.util.concurrent.Future;
37 import java.util.concurrent.ScheduledExecutorService;
38 import java.util.concurrent.ScheduledFuture;
39 import java.util.concurrent.TimeUnit;
40 import java.util.concurrent.locks.ReentrantLock;
42 import org.eclipse.jdt.annotation.NonNullByDefault;
43 import org.eclipse.jdt.annotation.Nullable;
44 import org.openhab.binding.ipcamera.internal.AmcrestHandler;
45 import org.openhab.binding.ipcamera.internal.CameraConfig;
46 import org.openhab.binding.ipcamera.internal.ChannelTracking;
47 import org.openhab.binding.ipcamera.internal.DahuaHandler;
48 import org.openhab.binding.ipcamera.internal.DoorBirdHandler;
49 import org.openhab.binding.ipcamera.internal.Ffmpeg;
50 import org.openhab.binding.ipcamera.internal.FoscamHandler;
51 import org.openhab.binding.ipcamera.internal.GroupTracker;
52 import org.openhab.binding.ipcamera.internal.Helper;
53 import org.openhab.binding.ipcamera.internal.HikvisionHandler;
54 import org.openhab.binding.ipcamera.internal.HttpOnlyHandler;
55 import org.openhab.binding.ipcamera.internal.InstarHandler;
56 import org.openhab.binding.ipcamera.internal.IpCameraActions;
57 import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
58 import org.openhab.binding.ipcamera.internal.IpCameraDynamicStateDescriptionProvider;
59 import org.openhab.binding.ipcamera.internal.MyNettyAuthHandler;
60 import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection;
61 import org.openhab.binding.ipcamera.internal.servlet.CameraServlet;
62 import org.openhab.core.OpenHAB;
63 import org.openhab.core.library.types.DecimalType;
64 import org.openhab.core.library.types.IncreaseDecreaseType;
65 import org.openhab.core.library.types.OnOffType;
66 import org.openhab.core.library.types.PercentType;
67 import org.openhab.core.library.types.RawType;
68 import org.openhab.core.library.types.StringType;
69 import org.openhab.core.thing.ChannelUID;
70 import org.openhab.core.thing.Thing;
71 import org.openhab.core.thing.ThingStatus;
72 import org.openhab.core.thing.ThingStatusDetail;
73 import org.openhab.core.thing.binding.BaseThingHandler;
74 import org.openhab.core.thing.binding.ThingHandlerService;
75 import org.openhab.core.types.Command;
76 import org.openhab.core.types.RefreshType;
77 import org.openhab.core.types.State;
78 import org.osgi.service.http.HttpService;
79 import org.slf4j.Logger;
80 import org.slf4j.LoggerFactory;
82 import io.netty.bootstrap.Bootstrap;
83 import io.netty.buffer.ByteBuf;
84 import io.netty.buffer.Unpooled;
85 import io.netty.channel.Channel;
86 import io.netty.channel.ChannelDuplexHandler;
87 import io.netty.channel.ChannelFuture;
88 import io.netty.channel.ChannelFutureListener;
89 import io.netty.channel.ChannelHandlerContext;
90 import io.netty.channel.ChannelInitializer;
91 import io.netty.channel.ChannelOption;
92 import io.netty.channel.EventLoopGroup;
93 import io.netty.channel.group.ChannelGroup;
94 import io.netty.channel.group.DefaultChannelGroup;
95 import io.netty.channel.nio.NioEventLoopGroup;
96 import io.netty.channel.socket.SocketChannel;
97 import io.netty.channel.socket.nio.NioSocketChannel;
98 import io.netty.handler.codec.base64.Base64;
99 import io.netty.handler.codec.http.DefaultFullHttpRequest;
100 import io.netty.handler.codec.http.FullHttpRequest;
101 import io.netty.handler.codec.http.HttpClientCodec;
102 import io.netty.handler.codec.http.HttpContent;
103 import io.netty.handler.codec.http.HttpHeaderValues;
104 import io.netty.handler.codec.http.HttpMessage;
105 import io.netty.handler.codec.http.HttpMethod;
106 import io.netty.handler.codec.http.HttpResponse;
107 import io.netty.handler.codec.http.HttpVersion;
108 import io.netty.handler.codec.http.LastHttpContent;
109 import io.netty.handler.timeout.IdleState;
110 import io.netty.handler.timeout.IdleStateEvent;
111 import io.netty.handler.timeout.IdleStateHandler;
112 import io.netty.util.CharsetUtil;
113 import io.netty.util.ReferenceCountUtil;
114 import io.netty.util.concurrent.GlobalEventExecutor;
117 * The {@link IpCameraHandler} is responsible for handling commands, which are
118 * sent to one of the channels.
120 * @author Matthew Skinner - Initial contribution
124 public class IpCameraHandler extends BaseThingHandler {
125 public final Logger logger = LoggerFactory.getLogger(getClass());
126 public final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
127 private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2);
128 private GroupTracker groupTracker;
129 public CameraConfig cameraConfig = new CameraConfig();
131 // ChannelGroup is thread safe
132 public final ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
133 private final HttpService httpService;
134 private @Nullable CameraServlet servlet;
135 public String mjpegContentType = "";
136 public @Nullable Ffmpeg ffmpegHLS = null;
137 public @Nullable Ffmpeg ffmpegRecord = null;
138 public @Nullable Ffmpeg ffmpegGIF = null;
139 public @Nullable Ffmpeg ffmpegRtspHelper = null;
140 public @Nullable Ffmpeg ffmpegMjpeg = null;
141 public @Nullable Ffmpeg ffmpegSnapshot = null;
142 public boolean streamingAutoFps = false;
143 public boolean motionDetected = false;
144 public Instant lastSnapshotRequest = Instant.now();
145 public Instant currentSnapshotTime = Instant.now();
146 private @Nullable ScheduledFuture<?> cameraConnectionJob = null;
147 private @Nullable ScheduledFuture<?> pollCameraJob = null;
148 private @Nullable ScheduledFuture<?> snapshotJob = null;
149 private @Nullable Bootstrap mainBootstrap;
150 private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup(1);
151 private FullHttpRequest putRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
153 private String gifFilename = "ipcamera";
154 private String gifHistory = "";
155 private String mp4History = "";
156 public int gifHistoryLength;
157 public int mp4HistoryLength;
158 private String mp4Filename = "ipcamera";
159 private int mp4RecordTime;
160 private int gifRecordTime = 5;
161 private LinkedList<byte[]> fifoSnapshotBuffer = new LinkedList<byte[]>();
162 private int snapCount;
163 private boolean updateImageChannel = false;
164 private byte lowPriorityCounter = 0;
165 public String hostIp;
166 public Map<String, ChannelTracking> channelTrackingMap = new ConcurrentHashMap<>();
167 public List<String> lowPriorityRequests = new ArrayList<>(0);
169 // basicAuth MUST remain private as it holds the cameraConfig.getPassword()
170 private String basicAuth = "";
171 public boolean useBasicAuth = false;
172 public boolean useDigestAuth = false;
173 public String snapshotUri = "";
174 public String mjpegUri = "";
175 private byte[] currentSnapshot = new byte[] { (byte) 0x00 };
176 public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
177 public String rtspUri = "";
178 public boolean audioAlarmUpdateSnapshot = false;
179 private boolean motionAlarmUpdateSnapshot = false;
180 private boolean isOnline = false; // Used so only 1 error is logged when a network issue occurs.
181 private boolean firstAudioAlarm = false;
182 private boolean firstMotionAlarm = false;
183 public BigDecimal motionThreshold = BigDecimal.ZERO;
184 public int audioThreshold = 35;
185 public boolean streamingSnapshotMjpeg = false;
186 public boolean motionAlarmEnabled = false;
187 public boolean audioAlarmEnabled = false;
188 public boolean ffmpegSnapshotGeneration = false;
189 public boolean snapshotPolling = false;
190 public OnvifConnection onvifCamera = new OnvifConnection(this, "", "", "");
192 // These methods handle the response from all camera brands, nothing specific to 1 brand.
193 private class CommonCameraHandler extends ChannelDuplexHandler {
194 private int bytesToRecieve = 0;
195 private int bytesAlreadyRecieved = 0;
196 private byte[] incomingJpeg = new byte[0];
197 private String incomingMessage = "";
198 private String contentType = "empty";
199 private String boundary = "";
200 private Object reply = new Object();
201 private String requestUrl = "";
202 private boolean isChunked = false;
204 public void setURL(String url) {
209 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
210 if (msg == null || ctx == null) {
214 if (msg instanceof HttpResponse) {
215 HttpResponse response = (HttpResponse) msg;
216 if (response.status().code() == 200) {
217 if (!response.headers().isEmpty()) {
218 for (String name : response.headers().names()) {
219 // Some cameras use first letter uppercase and others dont.
220 switch (name.toLowerCase()) { // Possible localization issues doing this
222 contentType = response.headers().getAsString(name);
224 case "content-length":
225 bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
227 case "transfer-encoding":
228 if (response.headers().getAsString(name).contains("chunked")) {
234 if (contentType.contains("multipart")) {
235 boundary = Helper.searchString(contentType, "boundary=");
236 if (mjpegUri.equals(requestUrl)) {
237 if (msg instanceof HttpMessage) {
238 // very start of stream only
239 mjpegContentType = contentType;
240 CameraServlet localServlet = servlet;
241 if (localServlet != null) {
242 logger.debug("Setting Content-Type to:{}", contentType);
243 localServlet.openStreams.updateContentType(contentType, boundary);
247 } else if (contentType.contains("image/jp")) {
248 if (bytesToRecieve == 0) {
249 bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
250 logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
252 incomingJpeg = new byte[bytesToRecieve];
256 // Non 200 OK replies are logged and handled in pipeline by MyNettyAuthHandler.java
260 if (msg instanceof HttpContent) {
261 HttpContent content = (HttpContent) msg;
262 if (mjpegUri.equals(requestUrl) && !(content instanceof LastHttpContent)) {
263 // multiple MJPEG stream packets come back as this.
264 byte[] chunkedFrame = new byte[content.content().readableBytes()];
265 content.content().getBytes(content.content().readerIndex(), chunkedFrame);
266 CameraServlet localServlet = servlet;
267 if (localServlet != null) {
268 localServlet.openStreams.queueFrame(chunkedFrame);
271 // Found some cameras use Content-Type: image/jpg instead of image/jpeg
272 if (contentType.contains("image/jp")) {
273 for (int i = 0; i < content.content().capacity(); i++) {
274 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
276 if (content instanceof LastHttpContent) {
277 processSnapshot(incomingJpeg);
280 } else { // incomingMessage that is not an IMAGE
281 if (incomingMessage.isEmpty()) {
282 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
284 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
286 bytesAlreadyRecieved = incomingMessage.length();
287 if (content instanceof LastHttpContent) {
288 // If it is not an image send it on to the next handler//
289 if (bytesAlreadyRecieved != 0) {
290 reply = incomingMessage;
291 super.channelRead(ctx, reply);
294 // Alarm Streams never have a LastHttpContent as they always stay open//
295 else if (contentType.contains("multipart")) {
296 int beginIndex, endIndex;
297 if (bytesToRecieve == 0) {
298 beginIndex = incomingMessage.indexOf("Content-Length:");
299 if (beginIndex != -1) {
300 endIndex = incomingMessage.indexOf("\r\n", beginIndex);
301 if (endIndex != -1) {
302 bytesToRecieve = Integer.parseInt(
303 incomingMessage.substring(beginIndex + 15, endIndex).strip());
307 // --boundary and headers are not included in the Content-Length value
308 if (bytesAlreadyRecieved > bytesToRecieve) {
309 // Check if message has a second --boundary
310 endIndex = incomingMessage.indexOf("--" + boundary, bytesToRecieve);
311 if (endIndex == -1) {
312 reply = incomingMessage;
313 incomingMessage = "";
315 bytesAlreadyRecieved = 0;
317 reply = incomingMessage.substring(0, endIndex);
318 incomingMessage = incomingMessage.substring(endIndex, incomingMessage.length());
319 bytesToRecieve = 0;// Triggers search next time for Content-Length:
320 bytesAlreadyRecieved = incomingMessage.length() - endIndex;
322 super.channelRead(ctx, reply);
325 // Foscam needs this as will other cameras with chunks//
326 if (isChunked && bytesAlreadyRecieved != 0) {
327 logger.debug("Reply is chunked.");
328 reply = incomingMessage;
329 super.channelRead(ctx, reply);
333 } else { // msg is not HttpContent
334 // Foscam cameras need this
335 if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
336 reply = incomingMessage;
337 logger.debug("Packet back from camera is {}", incomingMessage);
338 super.channelRead(ctx, reply);
342 ReferenceCountUtil.release(msg);
347 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
348 if (cause == null || ctx == null) {
351 if (cause instanceof ArrayIndexOutOfBoundsException) {
352 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
355 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
362 @SuppressWarnings("PMD.CompareObjectsWithEquals")
363 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
367 if (evt instanceof IdleStateEvent) {
368 IdleStateEvent e = (IdleStateEvent) evt;
369 // If camera does not use the channel for X amount of time it will close.
370 if (e.state() == IdleState.READER_IDLE) {
371 String urlToKeepOpen = "";
372 switch (thing.getThingTypeUID().getId()) {
374 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
377 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
380 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
381 if (channelTracking != null) {
382 if (channelTracking.getChannel() == ctx.channel()) {
383 return; // don't auto close this as it is for the alarms.
386 logger.debug("Closing an idle channel for camera:{}", cameraConfig.getIp());
393 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
394 IpCameraDynamicStateDescriptionProvider stateDescriptionProvider, HttpService httpService) {
396 this.stateDescriptionProvider = stateDescriptionProvider;
397 if (ipAddress != null) {
400 hostIp = Helper.getLocalIpAddress();
402 this.groupTracker = groupTracker;
403 this.httpService = httpService;
406 private IpCameraHandler getHandle() {
410 // false clears the stored user/pass hash, true creates the hash
411 public boolean setBasicAuth(boolean useBasic) {
413 logger.debug("Clearing out the stored BASIC auth now.");
416 } else if (!basicAuth.isEmpty()) {
417 // If the binding sends multiple requests before basicAuth was set, this may trigger falsely.
418 logger.warn("Camera is reporting your username and/or password is wrong.");
421 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
422 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
423 ByteBuf byteBuf = null;
425 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
426 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
428 if (byteBuf != null) {
434 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
439 private String getCorrectUrlFormat(String longUrl) {
440 String temp = longUrl;
443 if (longUrl.isEmpty() || "ffmpeg".equals(longUrl)) {
448 url = new URL(longUrl);
449 int port = url.getPort();
451 if (url.getQuery() == null) {
452 temp = url.getPath();
454 temp = url.getPath() + "?" + url.getQuery();
457 if (url.getQuery() == null) {
458 temp = ":" + url.getPort() + url.getPath();
460 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
463 } catch (MalformedURLException e) {
464 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
469 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
470 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
471 sendHttpRequest("PUT", httpRequestURL, null);
474 public void sendHttpGET(String httpRequestURL) {
475 sendHttpRequest("GET", httpRequestURL, null);
478 public int getPortFromShortenedUrl(String httpRequestURL) {
479 if (httpRequestURL.startsWith(":")) {
480 int end = httpRequestURL.indexOf("/");
481 return Integer.parseInt(httpRequestURL.substring(1, end));
483 return cameraConfig.getPort();
486 public String getTinyUrl(String httpRequestURL) {
487 if (httpRequestURL.startsWith(":")) {
488 int beginIndex = httpRequestURL.indexOf("/");
489 return httpRequestURL.substring(beginIndex);
491 return httpRequestURL;
494 private void checkCameraConnection() {
495 if (snapshotPolling) {// Currently polling a real URL for snapshots, so camera must be online.
497 } else if (ffmpegSnapshotGeneration) {// Use RTSP stream creating snapshots to know camera is online.
498 Ffmpeg localSnapshot = ffmpegSnapshot;
499 if (localSnapshot != null && !localSnapshot.getIsAlive()) {
500 cameraCommunicationError("FFmpeg Snapshots Stopped: Check your camera can be reached.");
503 return;// ffmpeg snapshot stream is still alive
505 // Open a HTTP connection without sending any requests as we do not need a snapshot.
506 Bootstrap localBootstrap = mainBootstrap;
507 if (localBootstrap != null) {
508 ChannelFuture chFuture = localBootstrap
509 .connect(new InetSocketAddress(cameraConfig.getIp(), cameraConfig.getPort()));
510 if (chFuture.awaitUninterruptibly(500)) {
511 chFuture.channel().close();
515 cameraCommunicationError(
516 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
519 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
520 // The authHandler will generate a digest string and re-send using this same function when needed.
521 @SuppressWarnings("null")
522 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
523 int port = getPortFromShortenedUrl(httpRequestURLFull);
524 String httpRequestURL = getTinyUrl(httpRequestURLFull);
526 if (mainBootstrap == null) {
527 mainBootstrap = new Bootstrap();
528 mainBootstrap.group(mainEventLoopGroup);
529 mainBootstrap.channel(NioSocketChannel.class);
530 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
531 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
532 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
533 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
534 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
535 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
538 public void initChannel(SocketChannel socketChannel) throws Exception {
539 // HIK Alarm stream needs > 9sec idle to stop stream closing
540 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
541 socketChannel.pipeline().addLast(new HttpClientCodec());
542 socketChannel.pipeline().addLast(AUTH_HANDLER,
543 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
544 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
546 switch (thing.getThingTypeUID().getId()) {
548 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
551 socketChannel.pipeline()
552 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
555 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
558 socketChannel.pipeline().addLast(
559 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
561 case HIKVISION_THING:
562 socketChannel.pipeline()
563 .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
566 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
569 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
576 FullHttpRequest request;
577 if (!"PUT".equals(httpMethod) || (useDigestAuth && digestString == null)) {
578 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
579 request.headers().set("Host", cameraConfig.getIp() + ":" + port);
580 request.headers().set("Connection", HttpHeaderValues.CLOSE);
582 request = putRequestWithBody;
585 if (!basicAuth.isEmpty()) {
587 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
590 request.headers().set("Authorization", "Basic " + basicAuth);
595 if (digestString != null) {
596 request.headers().set("Authorization", "Digest " + digestString);
600 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
601 .addListener(new ChannelFutureListener() {
604 public void operationComplete(@Nullable ChannelFuture future) {
605 if (future == null) {
608 if (future.isDone() && future.isSuccess()) {
609 Channel ch = future.channel();
610 openChannels.add(ch);
614 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
617 openChannel(ch, httpRequestURL);
618 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
619 commonHandler.setURL(httpRequestURLFull);
620 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
621 authHandler.setURL(httpMethod, httpRequestURL);
623 switch (thing.getThingTypeUID().getId()) {
625 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
626 amcrestHandler.setURL(httpRequestURL);
629 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
630 instarHandler.setURL(httpRequestURL);
633 ch.writeAndFlush(request);
634 } else { // an error occured
635 cameraCommunicationError(
636 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
642 public void processSnapshot(byte[] incommingSnapshot) {
643 lockCurrentSnapshot.lock();
645 currentSnapshot = incommingSnapshot;
646 if (cameraConfig.getGifPreroll() > 0) {
647 fifoSnapshotBuffer.add(incommingSnapshot);
648 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
649 fifoSnapshotBuffer.removeFirst();
653 lockCurrentSnapshot.unlock();
654 currentSnapshotTime = Instant.now();
657 if (updateImageChannel) {
658 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
659 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
660 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
661 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
662 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
663 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
664 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
668 public void startStreamServer() {
669 servlet = new CameraServlet(this, httpService);
670 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
671 + getThing().getUID().getId() + "/ipcamera.m3u8"));
672 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
673 + getThing().getUID().getId() + "/ipcamera.jpg"));
674 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
675 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
678 public void openCamerasStream() {
679 if (mjpegUri.isEmpty() || "ffmpeg".equals(mjpegUri)) {
680 setupFfmpegFormat(FFmpegFormat.MJPEG);
683 closeChannel(getTinyUrl(mjpegUri));
684 // Dahua cameras crash if you refresh (close and open) the stream without this delay.
685 mainEventLoopGroup.schedule(this::openMjpegStream, 300, TimeUnit.MILLISECONDS);
688 private void openMjpegStream() {
689 sendHttpGET(mjpegUri);
692 private void openChannel(Channel channel, String httpRequestURL) {
693 ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
694 if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
695 tracker.setChannel(channel);
698 channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
701 public void closeChannel(String url) {
702 ChannelTracking channelTracking = channelTrackingMap.get(url);
703 if (channelTracking != null) {
704 if (channelTracking.getChannel().isOpen()) {
705 channelTracking.getChannel().close();
712 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
713 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
716 @SuppressWarnings("PMD.CompareObjectsWithEquals")
717 private void cleanChannels() {
718 for (Channel channel : openChannels) {
719 boolean oldChannel = true;
720 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
721 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
722 channelTrackingMap.remove(channelTracking.getRequestUrl());
724 if (channelTracking.getChannel() == channel) {
725 logger.debug("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
735 public void storeHttpReply(String url, String content) {
736 ChannelTracking channelTracking = channelTrackingMap.get(url);
737 if (channelTracking != null) {
738 channelTracking.setReply(content);
742 private void storeSnapshots() {
744 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
745 lockCurrentSnapshot.lock();
747 for (byte[] foo : fifoSnapshotBuffer) {
748 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
751 OutputStream fos = new FileOutputStream(file);
754 } catch (FileNotFoundException e) {
755 logger.warn("FileNotFoundException {}", e.getMessage());
756 } catch (IOException e) {
757 logger.warn("IOException {}", e.getMessage());
761 lockCurrentSnapshot.unlock();
765 public void setupFfmpegFormat(FFmpegFormat format) {
766 String inputOptions = cameraConfig.getFfmpegInputOptions();
767 if (cameraConfig.getFfmpegOutput().isEmpty()) {
768 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
771 if (rtspUri.isEmpty()) {
772 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
775 if (cameraConfig.getFfmpegLocation().isEmpty()) {
776 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
779 if (rtspUri.toLowerCase().contains("rtsp")) {
780 if (inputOptions.isEmpty()) {
781 inputOptions = "-rtsp_transport tcp";
785 // Make sure the folder exists, if not create it.
786 new File(cameraConfig.getFfmpegOutput()).mkdirs();
789 if (ffmpegHLS == null) {
790 if (!inputOptions.isEmpty()) {
791 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
792 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
793 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
794 cameraConfig.getUser(), cameraConfig.getPassword());
796 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
797 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
798 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
799 cameraConfig.getPassword());
802 Ffmpeg localHLS = ffmpegHLS;
803 if (localHLS != null) {
804 localHLS.startConverting();
808 if (cameraConfig.getGifPreroll() > 0) {
809 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
810 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
811 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
812 + cameraConfig.getGifOutOptions(),
813 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
814 cameraConfig.getPassword());
816 if (!inputOptions.isEmpty()) {
817 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
819 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
821 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
822 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
823 cameraConfig.getUser(), cameraConfig.getPassword());
825 if (cameraConfig.getGifPreroll() > 0) {
828 Ffmpeg localGIF = ffmpegGIF;
829 if (localGIF != null) {
830 localGIF.startConverting();
831 if (gifHistory.isEmpty()) {
832 gifHistory = gifFilename;
833 } else if (!"ipcamera".equals(gifFilename)) {
834 gifHistory = gifFilename + "," + gifHistory;
835 if (gifHistoryLength > 49) {
836 int endIndex = gifHistory.lastIndexOf(",");
837 gifHistory = gifHistory.substring(0, endIndex);
840 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
844 if (!inputOptions.isEmpty()) {
845 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
847 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
849 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
850 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
851 cameraConfig.getUser(), cameraConfig.getPassword());
852 Ffmpeg localRecord = ffmpegRecord;
853 if (localRecord != null) {
854 localRecord.startConverting();
855 if (mp4History.isEmpty()) {
856 mp4History = mp4Filename;
857 } else if (!"ipcamera".equals(mp4Filename)) {
858 mp4History = mp4Filename + "," + mp4History;
859 if (mp4HistoryLength > 49) {
860 int endIndex = mp4History.lastIndexOf(",");
861 mp4History = mp4History.substring(0, endIndex);
865 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
868 Ffmpeg localAlarms = ffmpegRtspHelper;
869 if (localAlarms != null) {
870 localAlarms.stopConverting();
871 if (!audioAlarmEnabled && !motionAlarmEnabled) {
875 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
876 String filterOptions = "";
877 if (!audioAlarmEnabled) {
878 filterOptions = "-an";
880 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
882 if (!motionAlarmEnabled && !ffmpegSnapshotGeneration) {
883 filterOptions = filterOptions.concat(" -vn");
884 } else if (motionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
885 String usersMotionOptions = cameraConfig.getMotionOptions();
886 if (usersMotionOptions.startsWith("-")) {
887 // Need to put the users custom options first in the chain before the motion is detected
888 filterOptions += " " + usersMotionOptions + ",select='gte(scene,"
889 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
891 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
892 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
894 } else if (motionAlarmEnabled) {
895 filterOptions = filterOptions.concat(" -vf select='gte(scene,"
896 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print");
898 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
899 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
900 localAlarms = ffmpegRtspHelper;
901 if (localAlarms != null) {
902 localAlarms.startConverting();
906 if (ffmpegMjpeg == null) {
907 if (inputOptions.isEmpty()) {
908 inputOptions = "-hide_banner -loglevel warning";
910 inputOptions += " -hide_banner -loglevel warning";
912 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
913 cameraConfig.getMjpegOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
914 + getThing().getUID().getId() + "/ipcamera.jpg",
915 cameraConfig.getUser(), cameraConfig.getPassword());
917 Ffmpeg localMjpeg = ffmpegMjpeg;
918 if (localMjpeg != null) {
919 localMjpeg.startConverting();
923 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
924 if (ffmpegSnapshot == null) {
925 if (inputOptions.isEmpty()) {
927 inputOptions = "-threads 1 -skip_frame nokey -hide_banner -loglevel warning";
929 inputOptions += " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
931 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
932 cameraConfig.getSnapshotOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
933 + getThing().getUID().getId() + "/snapshot.jpg",
934 cameraConfig.getUser(), cameraConfig.getPassword());
936 Ffmpeg localSnaps = ffmpegSnapshot;
937 if (localSnaps != null) {
938 localSnaps.startConverting();
944 public void noMotionDetected(String thisAlarmsChannel) {
945 setChannelState(thisAlarmsChannel, OnOffType.OFF);
946 firstMotionAlarm = false;
947 motionAlarmUpdateSnapshot = false;
948 motionDetected = false;
949 if (streamingAutoFps) {
950 stopSnapshotPolling();
951 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
952 stopSnapshotPolling();
957 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
958 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
959 * tampering with the camera.
961 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
962 updateState(thisAlarmsChannel, state);
965 public void motionDetected(String thisAlarmsChannel) {
966 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
967 updateState(thisAlarmsChannel, OnOffType.ON);
968 motionDetected = true;
969 if (streamingAutoFps) {
970 startSnapshotPolling();
972 if (cameraConfig.getUpdateImageWhen().contains("2")) {
973 if (!firstMotionAlarm) {
974 if (!snapshotUri.isEmpty()) {
977 firstMotionAlarm = true;// reset back to false when the jpg arrives.
979 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
980 if (!snapshotPolling) {
981 startSnapshotPolling();
983 firstMotionAlarm = true;
984 motionAlarmUpdateSnapshot = true;
988 public void audioDetected() {
989 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
990 if (cameraConfig.getUpdateImageWhen().contains("3")) {
991 if (!firstAudioAlarm) {
992 if (!snapshotUri.isEmpty()) {
995 firstAudioAlarm = true;// reset back to false when the jpg arrives.
997 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
998 firstAudioAlarm = true;
999 audioAlarmUpdateSnapshot = true;
1003 public void noAudioDetected() {
1004 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1005 firstAudioAlarm = false;
1006 audioAlarmUpdateSnapshot = false;
1009 public void recordMp4(String filename, int seconds) {
1010 mp4Filename = filename;
1011 mp4RecordTime = seconds;
1012 setupFfmpegFormat(FFmpegFormat.RECORD);
1013 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1016 public void recordGif(String filename, int seconds) {
1017 gifFilename = filename;
1018 gifRecordTime = seconds;
1019 if (cameraConfig.getGifPreroll() > 0) {
1020 snapCount = seconds;
1022 setupFfmpegFormat(FFmpegFormat.GIF);
1024 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1027 public String returnValueFromString(String rawString, String searchedString) {
1029 int index = rawString.indexOf(searchedString);
1030 if (index != -1) // -1 means "not found"
1032 result = rawString.substring(index + searchedString.length(), rawString.length());
1033 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1035 return result; // Did not find a carriage return.
1037 return result.substring(0, index);
1040 return ""; // Did not find the String we were searching for
1043 private void sendPTZRequest() {
1044 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1048 public void channelLinked(ChannelUID channelUID) {
1049 switch (channelUID.getId()) {
1050 case CHANNEL_MJPEG_URL:
1051 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1052 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
1054 case CHANNEL_HLS_URL:
1055 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1056 + getThing().getUID().getId() + "/ipcamera.m3u8"));
1058 case CHANNEL_IMAGE_URL:
1059 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1060 + getThing().getUID().getId() + "/ipcamera.jpg"));
1066 public void handleCommand(ChannelUID channelUID, Command command) {
1067 if (command instanceof RefreshType) {
1068 switch (channelUID.getId()) {
1070 if (onvifCamera.supportsPTZ()) {
1071 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1075 if (onvifCamera.supportsPTZ()) {
1076 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1080 if (onvifCamera.supportsPTZ()) {
1081 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1084 case CHANNEL_GOTO_PRESET:
1085 if (onvifCamera.supportsPTZ()) {
1086 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1090 } // caution "REFRESH" can still progress to brand Handlers below the else.
1092 switch (channelUID.getId()) {
1093 case CHANNEL_MP4_HISTORY_LENGTH:
1094 if (DecimalType.ZERO.equals(command)) {
1095 mp4HistoryLength = 0;
1097 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1100 case CHANNEL_GIF_HISTORY_LENGTH:
1101 if (DecimalType.ZERO.equals(command)) {
1102 gifHistoryLength = 0;
1104 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1107 case CHANNEL_FFMPEG_MOTION_CONTROL:
1108 if (OnOffType.ON.equals(command)) {
1109 motionAlarmEnabled = true;
1110 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1111 motionAlarmEnabled = false;
1112 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1113 } else if (command instanceof PercentType) {
1114 motionAlarmEnabled = true;
1115 motionThreshold = ((PercentType) command).toBigDecimal();
1117 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1119 case CHANNEL_START_STREAM:
1121 if (OnOffType.ON.equals(command)) {
1122 localHLS = ffmpegHLS;
1123 if (localHLS == null) {
1124 setupFfmpegFormat(FFmpegFormat.HLS);
1125 localHLS = ffmpegHLS;
1127 if (localHLS != null) {
1128 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1129 localHLS.startConverting();
1132 localHLS = ffmpegHLS;
1133 if (localHLS != null) {
1134 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1135 localHLS.setKeepAlive(1);
1139 case CHANNEL_EXTERNAL_MOTION:
1140 if (OnOffType.ON.equals(command)) {
1141 motionDetected(CHANNEL_EXTERNAL_MOTION);
1143 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1146 case CHANNEL_GOTO_PRESET:
1147 if (onvifCamera.supportsPTZ()) {
1148 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1151 case CHANNEL_POLL_IMAGE:
1152 if (OnOffType.ON.equals(command)) {
1153 if (snapshotUri.isEmpty()) {
1154 ffmpegSnapshotGeneration = true;
1155 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1156 updateImageChannel = false;
1158 updateImageChannel = true;
1159 updateSnapshot();// Allows this to change Image FPS on demand
1162 Ffmpeg localSnaps = ffmpegSnapshot;
1163 if (localSnaps != null) {
1164 localSnaps.stopConverting();
1165 ffmpegSnapshotGeneration = false;
1167 updateImageChannel = false;
1171 if (onvifCamera.supportsPTZ()) {
1172 if (command instanceof IncreaseDecreaseType) {
1173 if (command == IncreaseDecreaseType.INCREASE) {
1174 if (cameraConfig.getPtzContinuous()) {
1175 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1177 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1180 if (cameraConfig.getPtzContinuous()) {
1181 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1183 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1187 } else if (OnOffType.OFF.equals(command)) {
1188 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1191 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1192 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1196 if (onvifCamera.supportsPTZ()) {
1197 if (command instanceof IncreaseDecreaseType) {
1198 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1199 if (cameraConfig.getPtzContinuous()) {
1200 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1202 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1205 if (cameraConfig.getPtzContinuous()) {
1206 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1208 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1212 } else if (OnOffType.OFF.equals(command)) {
1213 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1216 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1217 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1221 if (onvifCamera.supportsPTZ()) {
1222 if (command instanceof IncreaseDecreaseType) {
1223 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1224 if (cameraConfig.getPtzContinuous()) {
1225 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1227 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1230 if (cameraConfig.getPtzContinuous()) {
1231 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1233 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1237 } else if (OnOffType.OFF.equals(command)) {
1238 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1241 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1242 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1247 // commands and refresh now get passed to brand handlers
1248 switch (thing.getThingTypeUID().getId()) {
1250 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1251 amcrestHandler.handleCommand(channelUID, command);
1252 if (lowPriorityRequests.isEmpty()) {
1253 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1257 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1258 dahuaHandler.handleCommand(channelUID, command);
1259 if (lowPriorityRequests.isEmpty()) {
1260 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1263 case DOORBIRD_THING:
1264 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1265 doorBirdHandler.handleCommand(channelUID, command);
1266 if (lowPriorityRequests.isEmpty()) {
1267 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1270 case HIKVISION_THING:
1271 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1272 hikvisionHandler.handleCommand(channelUID, command);
1273 if (lowPriorityRequests.isEmpty()) {
1274 lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1278 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1279 cameraConfig.getPassword());
1280 foscamHandler.handleCommand(channelUID, command);
1281 if (lowPriorityRequests.isEmpty()) {
1282 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1286 InstarHandler instarHandler = new InstarHandler(getHandle());
1287 instarHandler.handleCommand(channelUID, command);
1288 if (lowPriorityRequests.isEmpty()) {
1289 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1293 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1294 defaultHandler.handleCommand(channelUID, command);
1295 if (lowPriorityRequests.isEmpty()) {
1296 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1302 public void setChannelState(String channelToUpdate, State valueOf) {
1303 updateState(channelToUpdate, valueOf);
1306 private void bringCameraOnline() {
1308 updateStatus(ThingStatus.ONLINE);
1309 groupTracker.listOfOnlineCameraHandlers.add(this);
1310 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1311 Future<?> localFuture = cameraConnectionJob;
1312 if (localFuture != null) {
1313 localFuture.cancel(false);
1314 cameraConnectionJob = null;
1316 if (!snapshotUri.isEmpty()) {
1317 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1318 snapshotPolling = true;
1319 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000,
1320 cameraConfig.getPollTime(), TimeUnit.MILLISECONDS);
1324 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1326 // auto restart mjpeg stream now camera is back online.
1327 CameraServlet localServlet = servlet;
1328 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1329 openCamerasStream();
1332 if (!rtspUri.isEmpty()) {
1333 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1335 if (updateImageChannel) {
1336 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1338 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1340 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1341 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1342 handle.cameraOnline(getThing().getUID().getId());
1347 void snapshotIsFfmpeg() {
1348 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1350 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1351 bringCameraOnline();
1352 if (!rtspUri.isEmpty()) {
1353 updateImageChannel = false;
1354 ffmpegSnapshotGeneration = true;
1355 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1356 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1358 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1362 void pollingCameraConnection() {
1364 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)
1365 || thing.getThingTypeUID().getId().equals(DOORBIRD_THING)) {
1366 if (rtspUri.isEmpty()) {
1367 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1369 if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1372 ffmpegSnapshotGeneration = false;
1377 if (!onvifCamera.isConnected()) {
1378 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1379 cameraConfig.getOnvifPort());
1380 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1382 if ("ffmpeg".equals(snapshotUri)) {
1384 } else if (!snapshotUri.isEmpty()) {
1385 ffmpegSnapshotGeneration = false;
1387 } else if (!rtspUri.isEmpty()) {
1390 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1391 "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.");
1395 public void cameraConfigError(String reason) {
1396 // wont try to reconnect again due to a config error being the cause.
1397 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1401 public void cameraCommunicationError(String reason) {
1402 // will try to reconnect again as camera may be rebooting.
1403 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1404 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1405 resetAndRetryConnecting();
1409 boolean streamIsStopped(String url) {
1410 ChannelTracking channelTracking = channelTrackingMap.get(url);
1411 if (channelTracking != null) {
1412 if (channelTracking.getChannel().isActive()) {
1413 return false; // stream is running.
1416 return true; // Stream stopped or never started.
1419 void snapshotRunnable() {
1420 // Snapshot should be first to keep consistent time between shots
1422 if (snapCount > 0) {
1423 if (--snapCount == 0) {
1424 setupFfmpegFormat(FFmpegFormat.GIF);
1429 private void takeSnapshot() {
1430 sendHttpGET(snapshotUri);
1433 private void updateSnapshot() {
1434 lastSnapshotRequest = Instant.now();
1435 mainEventLoopGroup.schedule(this::takeSnapshot, 0, TimeUnit.MILLISECONDS);
1438 public byte[] getSnapshot() {
1440 // Single gray pixel JPG to keep streams open when the camera goes offline so they dont stop.
1441 return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
1442 0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43,
1443 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
1444 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09,
1445 0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d,
1446 0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10,
1447 0x10, (byte) 0xff, (byte) 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
1448 (byte) 0xff, (byte) 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, (byte) 0xff, (byte) 0xda, 0x00, 0x08,
1449 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, (byte) 0xd2, (byte) 0xcf, 0x20, (byte) 0xff, (byte) 0xd9 };
1451 // Most cameras will return a 503 busy error if snapshot is faster than 1 second
1452 long lastUpdatedMs = Duration.between(lastSnapshotRequest, Instant.now()).toMillis();
1453 if (!snapshotPolling && !ffmpegSnapshotGeneration && lastUpdatedMs >= cameraConfig.getPollTime()) {
1456 lockCurrentSnapshot.lock();
1458 return currentSnapshot;
1460 lockCurrentSnapshot.unlock();
1464 public void stopSnapshotPolling() {
1465 Future<?> localFuture;
1466 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1467 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1468 snapshotPolling = false;
1469 localFuture = snapshotJob;
1470 if (localFuture != null) {
1471 localFuture.cancel(true);
1473 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1474 snapshotPolling = false;
1475 localFuture = snapshotJob;
1476 if (localFuture != null) {
1477 localFuture.cancel(true);
1482 public void startSnapshotPolling() {
1483 if (snapshotPolling || ffmpegSnapshotGeneration) {
1484 return; // Already polling or creating with FFmpeg from RTSP
1486 if (streamingSnapshotMjpeg || streamingAutoFps || cameraConfig.getUpdateImageWhen().contains("4")) {
1487 snapshotPolling = true;
1488 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 0, cameraConfig.getPollTime(),
1489 TimeUnit.MILLISECONDS);
1494 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep alarm
1495 * streams open and more.
1498 void pollCameraRunnable() {
1499 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1500 if (!lowPriorityRequests.isEmpty()) {
1501 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1502 lowPriorityCounter = 0;
1504 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1506 // what needs to be done every poll//
1507 switch (thing.getThingTypeUID().getId()) {
1509 if (!snapshotPolling) {
1510 checkCameraConnection();
1514 if (!snapshotPolling) {
1515 checkCameraConnection();
1517 if (!onvifCamera.isConnected()) {
1518 onvifCamera.connect(true);
1522 if (!snapshotPolling) {
1523 checkCameraConnection();
1525 noMotionDetected(CHANNEL_MOTION_ALARM);
1526 noMotionDetected(CHANNEL_PIR_ALARM);
1529 case HIKVISION_THING:
1530 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1531 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1532 cameraConfig.getIp());
1533 sendHttpGET("/ISAPI/Event/notification/alertStream");
1537 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1538 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1541 if (!snapshotPolling) {
1542 checkCameraConnection();
1544 // Check for alarms, channel for NVRs appears not to work at filtering.
1545 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1546 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1547 cameraConfig.getIp());
1548 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1551 case DOORBIRD_THING:
1552 if (!snapshotPolling) {
1553 checkCameraConnection();
1555 // Check for alarms, channel for NVRs appears not to work at filtering.
1556 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1557 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1558 cameraConfig.getIp());
1559 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1563 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1564 + cameraConfig.getPassword());
1567 Ffmpeg localHLS = ffmpegHLS;
1568 if (localHLS != null) {
1569 localHLS.checkKeepAlive();
1571 if (openChannels.size() > 10) {
1572 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1578 public void initialize() {
1579 cameraConfig = getConfigAs(CameraConfig.class);
1580 threadPool = Executors.newScheduledThreadPool(2);
1581 mainEventLoopGroup = new NioEventLoopGroup(3);
1582 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1583 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1584 rtspUri = cameraConfig.getFfmpegInput();
1585 if (cameraConfig.getFfmpegOutput().isEmpty()) {
1587 .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1589 // Known cameras will connect quicker if we skip ONVIF questions.
1590 switch (thing.getThingTypeUID().getId()) {
1593 if (mjpegUri.isEmpty()) {
1594 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1596 if (snapshotUri.isEmpty()) {
1597 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1600 case DOORBIRD_THING:
1601 if (mjpegUri.isEmpty()) {
1602 mjpegUri = "/bha-api/video.cgi";
1604 if (snapshotUri.isEmpty()) {
1605 snapshotUri = "/bha-api/image.cgi";
1609 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1610 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1611 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1612 if (mjpegUri.isEmpty()) {
1613 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1614 + cameraConfig.getPassword();
1616 if (snapshotUri.isEmpty()) {
1617 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1618 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1621 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1622 if (mjpegUri.isEmpty()) {
1623 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1625 if (snapshotUri.isEmpty()) {
1626 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1630 if (snapshotUri.isEmpty()) {
1631 snapshotUri = "/tmpfs/snap.jpg";
1633 if (mjpegUri.isEmpty()) {
1634 mjpegUri = "/mjpegstream.cgi?-chn=12";
1637 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
1638 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1639 + getThing().getUID().getId()
1640 + "/instar&-as_queryattr1=&-as_queryval1=&-as_queryattr2=&-as_queryval2=&-as_queryattr3=&-as_queryval3=&-as_activequery=1&-as_auth=0&-as_query1=0&-as_query2=0&-as_query3=0");
1643 // for poll times 9 seconds and above don't display a warning about the Image channel.
1644 if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1646 "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.");
1648 // ONVIF and Instar event handling need the server started before connecting.
1649 startStreamServer();
1653 private void tryConnecting() {
1654 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)
1655 && !thing.getThingTypeUID().getId().equals(DOORBIRD_THING)) {
1656 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1657 cameraConfig.getUser(), cameraConfig.getPassword());
1658 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1659 // Only use ONVIF events if it is not an API camera.
1660 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1662 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 8, TimeUnit.SECONDS);
1665 private void keepMjpegRunning() {
1666 CameraServlet localServlet = servlet;
1667 if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1668 if (!mjpegUri.isEmpty() && !"ffmpeg".equals(mjpegUri)) {
1669 localServlet.openStreams.queueFrame(("--" + localServlet.openStreams.boundary + "\r\n\r\n").getBytes());
1671 localServlet.openStreams.queueFrame(getSnapshot());
1675 // What the camera needs to re-connect if the initialize() is not called.
1676 private void resetAndRetryConnecting() {
1681 private void offline() {
1683 snapshotPolling = false;
1684 Future<?> localFuture = pollCameraJob;
1685 if (localFuture != null) {
1686 localFuture.cancel(true);
1689 localFuture = snapshotJob;
1690 if (localFuture != null) {
1691 localFuture.cancel(true);
1694 localFuture = cameraConnectionJob;
1695 if (localFuture != null) {
1696 localFuture.cancel(true);
1699 Ffmpeg localFfmpeg = ffmpegHLS;
1700 if (localFfmpeg != null) {
1701 localFfmpeg.stopConverting();
1704 localFfmpeg = ffmpegRecord;
1705 if (localFfmpeg != null) {
1706 localFfmpeg.stopConverting();
1707 ffmpegRecord = null;
1709 localFfmpeg = ffmpegGIF;
1710 if (localFfmpeg != null) {
1711 localFfmpeg.stopConverting();
1714 localFfmpeg = ffmpegRtspHelper;
1715 if (localFfmpeg != null) {
1716 localFfmpeg.stopConverting();
1717 ffmpegRtspHelper = null;
1719 localFfmpeg = ffmpegMjpeg;
1720 if (localFfmpeg != null) {
1721 localFfmpeg.stopConverting();
1724 localFfmpeg = ffmpegSnapshot;
1725 if (localFfmpeg != null) {
1726 localFfmpeg.stopConverting();
1727 ffmpegSnapshot = null;
1729 onvifCamera.disconnect();
1730 openChannels.close();
1734 public void dispose() {
1736 CameraServlet localServlet = servlet;
1737 if (localServlet != null) {
1738 localServlet.dispose();
1739 localServlet = null;
1741 threadPool.shutdown();
1742 // inform all group handlers that this camera has gone offline
1743 groupTracker.listOfOnlineCameraHandlers.remove(this);
1744 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1745 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1746 handle.cameraOffline(this);
1748 basicAuth = ""; // clear out stored Password hash
1749 useDigestAuth = false;
1750 mainEventLoopGroup.shutdownGracefully();
1751 mainBootstrap = null;
1752 channelTrackingMap.clear();
1755 public String getWhiteList() {
1756 return cameraConfig.getIpWhitelist();
1760 public Collection<Class<? extends ThingHandlerService>> getServices() {
1761 return Collections.singleton(IpCameraActions.class);