2 * Copyright (c) 2010-2021 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.net.InetSocketAddress;
23 import java.net.MalformedURLException;
25 import java.nio.charset.StandardCharsets;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.LinkedList;
30 import java.util.List;
32 import java.util.concurrent.ConcurrentHashMap;
33 import java.util.concurrent.Executors;
34 import java.util.concurrent.Future;
35 import java.util.concurrent.ScheduledExecutorService;
36 import java.util.concurrent.ScheduledFuture;
37 import java.util.concurrent.TimeUnit;
38 import java.util.concurrent.locks.ReentrantLock;
40 import org.eclipse.jdt.annotation.NonNullByDefault;
41 import org.eclipse.jdt.annotation.Nullable;
42 import org.openhab.binding.ipcamera.internal.AmcrestHandler;
43 import org.openhab.binding.ipcamera.internal.CameraConfig;
44 import org.openhab.binding.ipcamera.internal.ChannelTracking;
45 import org.openhab.binding.ipcamera.internal.DahuaHandler;
46 import org.openhab.binding.ipcamera.internal.DoorBirdHandler;
47 import org.openhab.binding.ipcamera.internal.Ffmpeg;
48 import org.openhab.binding.ipcamera.internal.FoscamHandler;
49 import org.openhab.binding.ipcamera.internal.GroupTracker;
50 import org.openhab.binding.ipcamera.internal.Helper;
51 import org.openhab.binding.ipcamera.internal.HikvisionHandler;
52 import org.openhab.binding.ipcamera.internal.HttpOnlyHandler;
53 import org.openhab.binding.ipcamera.internal.InstarHandler;
54 import org.openhab.binding.ipcamera.internal.IpCameraActions;
55 import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
56 import org.openhab.binding.ipcamera.internal.IpCameraDynamicStateDescriptionProvider;
57 import org.openhab.binding.ipcamera.internal.MyNettyAuthHandler;
58 import org.openhab.binding.ipcamera.internal.StreamServerHandler;
59 import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection;
60 import org.openhab.core.library.types.DecimalType;
61 import org.openhab.core.library.types.IncreaseDecreaseType;
62 import org.openhab.core.library.types.OnOffType;
63 import org.openhab.core.library.types.PercentType;
64 import org.openhab.core.library.types.RawType;
65 import org.openhab.core.library.types.StringType;
66 import org.openhab.core.thing.ChannelUID;
67 import org.openhab.core.thing.Thing;
68 import org.openhab.core.thing.ThingStatus;
69 import org.openhab.core.thing.ThingStatusDetail;
70 import org.openhab.core.thing.binding.BaseThingHandler;
71 import org.openhab.core.thing.binding.ThingHandlerService;
72 import org.openhab.core.types.Command;
73 import org.openhab.core.types.RefreshType;
74 import org.openhab.core.types.State;
75 import org.slf4j.Logger;
76 import org.slf4j.LoggerFactory;
78 import io.netty.bootstrap.Bootstrap;
79 import io.netty.bootstrap.ServerBootstrap;
80 import io.netty.buffer.ByteBuf;
81 import io.netty.buffer.Unpooled;
82 import io.netty.channel.Channel;
83 import io.netty.channel.ChannelDuplexHandler;
84 import io.netty.channel.ChannelFuture;
85 import io.netty.channel.ChannelFutureListener;
86 import io.netty.channel.ChannelHandlerContext;
87 import io.netty.channel.ChannelInitializer;
88 import io.netty.channel.ChannelOption;
89 import io.netty.channel.EventLoopGroup;
90 import io.netty.channel.group.ChannelGroup;
91 import io.netty.channel.group.DefaultChannelGroup;
92 import io.netty.channel.nio.NioEventLoopGroup;
93 import io.netty.channel.socket.SocketChannel;
94 import io.netty.channel.socket.nio.NioServerSocketChannel;
95 import io.netty.channel.socket.nio.NioSocketChannel;
96 import io.netty.handler.codec.base64.Base64;
97 import io.netty.handler.codec.http.DefaultFullHttpRequest;
98 import io.netty.handler.codec.http.DefaultHttpResponse;
99 import io.netty.handler.codec.http.FullHttpRequest;
100 import io.netty.handler.codec.http.HttpClientCodec;
101 import io.netty.handler.codec.http.HttpContent;
102 import io.netty.handler.codec.http.HttpHeaderNames;
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.HttpResponseStatus;
108 import io.netty.handler.codec.http.HttpServerCodec;
109 import io.netty.handler.codec.http.HttpVersion;
110 import io.netty.handler.codec.http.LastHttpContent;
111 import io.netty.handler.stream.ChunkedWriteHandler;
112 import io.netty.handler.timeout.IdleState;
113 import io.netty.handler.timeout.IdleStateEvent;
114 import io.netty.handler.timeout.IdleStateHandler;
115 import io.netty.util.CharsetUtil;
116 import io.netty.util.ReferenceCountUtil;
117 import io.netty.util.concurrent.GlobalEventExecutor;
120 * The {@link IpCameraHandler} is responsible for handling commands, which are
121 * sent to one of the channels.
123 * @author Matthew Skinner - Initial contribution
127 public class IpCameraHandler extends BaseThingHandler {
128 public final Logger logger = LoggerFactory.getLogger(getClass());
129 public final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
130 private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(4);
131 private GroupTracker groupTracker;
132 public CameraConfig cameraConfig = new CameraConfig();
134 // ChannelGroup is thread safe
135 public final ChannelGroup mjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
136 private final ChannelGroup snapshotMjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
137 private final ChannelGroup autoSnapshotMjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
138 public final ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
139 public @Nullable Ffmpeg ffmpegHLS = null;
140 public @Nullable Ffmpeg ffmpegRecord = null;
141 public @Nullable Ffmpeg ffmpegGIF = null;
142 public @Nullable Ffmpeg ffmpegRtspHelper = null;
143 public @Nullable Ffmpeg ffmpegMjpeg = null;
144 public @Nullable Ffmpeg ffmpegSnapshot = null;
145 public boolean streamingAutoFps = false;
146 public boolean motionDetected = false;
148 private @Nullable ScheduledFuture<?> cameraConnectionJob = null;
149 private @Nullable ScheduledFuture<?> pollCameraJob = null;
150 private @Nullable ScheduledFuture<?> snapshotJob = null;
151 private @Nullable Bootstrap mainBootstrap;
152 private @Nullable ServerBootstrap serverBootstrap;
154 private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup();
155 private EventLoopGroup serversLoopGroup = new NioEventLoopGroup();
156 private FullHttpRequest putRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
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 boolean updateAutoFps = false;
170 private byte lowPriorityCounter = 0;
171 public String hostIp;
172 public Map<String, ChannelTracking> channelTrackingMap = new ConcurrentHashMap<>();
173 public List<String> lowPriorityRequests = new ArrayList<>(0);
175 // basicAuth MUST remain private as it holds the cameraConfig.getPassword()
176 private String basicAuth = "";
177 public boolean useBasicAuth = false;
178 public boolean useDigestAuth = false;
179 public String snapshotUri = "";
180 public String mjpegUri = "";
181 private @Nullable ChannelFuture serverFuture = null;
182 private Object firstStreamedMsg = new Object();
183 public byte[] currentSnapshot = new byte[] { (byte) 0x00 };
184 public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
185 public String rtspUri = "";
186 public boolean audioAlarmUpdateSnapshot = false;
187 private boolean motionAlarmUpdateSnapshot = false;
188 private boolean isOnline = false; // Used so only 1 error is logged when a network issue occurs.
189 private boolean firstAudioAlarm = false;
190 private boolean firstMotionAlarm = false;
191 public Double motionThreshold = 0.0016;
192 public int audioThreshold = 35;
193 @SuppressWarnings("unused")
194 private @Nullable StreamServerHandler streamServerHandler;
195 private boolean streamingSnapshotMjpeg = false;
196 public boolean motionAlarmEnabled = false;
197 public boolean audioAlarmEnabled = false;
198 public boolean ffmpegSnapshotGeneration = false;
199 public boolean snapshotPolling = false;
200 public OnvifConnection onvifCamera = new OnvifConnection(this, "", "", "");
202 // These methods handle the response from all camera brands, nothing specific to 1 brand.
203 private class CommonCameraHandler extends ChannelDuplexHandler {
204 private int bytesToRecieve = 0;
205 private int bytesAlreadyRecieved = 0;
206 private byte[] incomingJpeg = new byte[0];
207 private String incomingMessage = "";
208 private String contentType = "empty";
209 private String boundary = "";
210 private Object reply = new Object();
211 private String requestUrl = "";
212 private boolean closeConnection = true;
213 private boolean isChunked = false;
215 public void setURL(String url) {
220 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
221 if (msg == null || ctx == null) {
225 if (msg instanceof HttpResponse) {
226 HttpResponse response = (HttpResponse) msg;
227 if (response.status().code() != 401) {
228 if (!response.headers().isEmpty()) {
229 for (String name : response.headers().names()) {
230 // Some cameras use first letter uppercase and others dont.
231 switch (name.toLowerCase()) { // Possible localization issues doing this
233 contentType = response.headers().getAsString(name);
235 case "content-length":
236 bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
239 if (response.headers().getAsString(name).contains("keep-alive")) {
240 closeConnection = false;
243 case "transfer-encoding":
244 if (response.headers().getAsString(name).contains("chunked")) {
250 if (contentType.contains("multipart")) {
251 closeConnection = false;
252 if (mjpegUri.equals(requestUrl)) {
253 if (msg instanceof HttpMessage) {
254 // very start of stream only
255 ReferenceCountUtil.retain(msg, 1);
256 firstStreamedMsg = msg;
257 streamToGroup(firstStreamedMsg, mjpegChannelGroup, true);
260 boundary = Helper.searchString(contentType, "boundary=");
262 } else if (contentType.contains("image/jp")) {
263 if (bytesToRecieve == 0) {
264 bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
265 logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
267 incomingJpeg = new byte[bytesToRecieve];
272 if (msg instanceof HttpContent) {
273 if (mjpegUri.equals(requestUrl)) {
274 // multiple MJPEG stream packets come back as this.
275 ReferenceCountUtil.retain(msg, 1);
276 streamToGroup(msg, mjpegChannelGroup, true);
278 HttpContent content = (HttpContent) msg;
279 // Found some cameras use Content-Type: image/jpg instead of image/jpeg
280 if (contentType.contains("image/jp")) {
281 for (int i = 0; i < content.content().capacity(); i++) {
282 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
284 if (content instanceof LastHttpContent) {
285 processSnapshot(incomingJpeg);
286 // testing next line and if works need to do a full cleanup of this function.
287 closeConnection = true;
288 if (closeConnection) {
292 bytesAlreadyRecieved = 0;
295 } else { // incomingMessage that is not an IMAGE
296 if (incomingMessage.isEmpty()) {
297 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
299 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
301 bytesAlreadyRecieved = incomingMessage.length();
302 if (content instanceof LastHttpContent) {
303 // If it is not an image send it on to the next handler//
304 if (bytesAlreadyRecieved != 0) {
305 reply = incomingMessage;
306 super.channelRead(ctx, reply);
309 // Alarm Streams never have a LastHttpContent as they always stay open//
310 else if (contentType.contains("multipart")) {
311 int beginIndex, endIndex;
312 if (bytesToRecieve == 0) {
313 beginIndex = incomingMessage.indexOf("Content-Length:");
314 if (beginIndex != -1) {
315 endIndex = incomingMessage.indexOf("\r\n", beginIndex);
316 if (endIndex != -1) {
317 bytesToRecieve = Integer.parseInt(
318 incomingMessage.substring(beginIndex + 15, endIndex).strip());
322 // --boundary and headers are not included in the Content-Length value
323 if (bytesAlreadyRecieved > bytesToRecieve) {
324 // Check if message has a second --boundary
325 endIndex = incomingMessage.indexOf("--" + boundary, bytesToRecieve);
326 if (endIndex == -1) {
327 reply = incomingMessage;
328 incomingMessage = "";
330 bytesAlreadyRecieved = 0;
332 reply = incomingMessage.substring(0, endIndex);
333 incomingMessage = incomingMessage.substring(endIndex, incomingMessage.length());
334 bytesToRecieve = 0;// Triggers search next time for Content-Length:
335 bytesAlreadyRecieved = incomingMessage.length() - endIndex;
337 super.channelRead(ctx, reply);
340 // Foscam needs this as will other cameras with chunks//
341 if (isChunked && bytesAlreadyRecieved != 0) {
342 logger.debug("Reply is chunked.");
343 reply = incomingMessage;
344 super.channelRead(ctx, reply);
348 } else { // msg is not HttpContent
349 // Foscam cameras need this
350 if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
351 reply = incomingMessage;
352 logger.debug("Packet back from camera is {}", incomingMessage);
353 super.channelRead(ctx, reply);
357 ReferenceCountUtil.release(msg);
362 public void channelReadComplete(@Nullable ChannelHandlerContext ctx) {
366 public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
370 public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
374 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
375 if (cause == null || ctx == null) {
378 if (cause instanceof ArrayIndexOutOfBoundsException) {
379 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
382 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
389 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
393 if (evt instanceof IdleStateEvent) {
394 IdleStateEvent e = (IdleStateEvent) evt;
395 // If camera does not use the channel for X amount of time it will close.
396 if (e.state() == IdleState.READER_IDLE) {
397 String urlToKeepOpen = "";
398 switch (thing.getThingTypeUID().getId()) {
400 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
402 case HIKVISION_THING:
403 urlToKeepOpen = "/ISAPI/Event/notification/alertStream";
406 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
409 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
410 if (channelTracking != null) {
411 if (channelTracking.getChannel() == ctx.channel()) {
412 return; // don't auto close this as it is for the alarms.
421 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
422 IpCameraDynamicStateDescriptionProvider stateDescriptionProvider) {
424 this.stateDescriptionProvider = stateDescriptionProvider;
425 if (ipAddress != null) {
428 hostIp = Helper.getLocalIpAddress();
430 this.groupTracker = groupTracker;
433 private IpCameraHandler getHandle() {
437 // false clears the stored user/pass hash, true creates the hash
438 public boolean setBasicAuth(boolean useBasic) {
440 logger.debug("Clearing out the stored BASIC auth now.");
443 } else if (!basicAuth.isEmpty()) {
444 // due to camera may have been sent multiple requests before the auth was set, this may trigger falsely.
445 logger.warn("Camera is reporting your username and/or password is wrong.");
448 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
449 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
450 ByteBuf byteBuf = null;
452 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
453 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
455 if (byteBuf != null) {
461 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
466 private String getCorrectUrlFormat(String longUrl) {
467 String temp = longUrl;
470 if (longUrl.isEmpty() || longUrl.equals("ffmpeg")) {
475 url = new URL(longUrl);
476 int port = url.getPort();
478 if (url.getQuery() == null) {
479 temp = url.getPath();
481 temp = url.getPath() + "?" + url.getQuery();
484 if (url.getQuery() == null) {
485 temp = ":" + url.getPort() + url.getPath();
487 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
490 } catch (MalformedURLException e) {
491 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
496 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
497 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
498 sendHttpRequest("PUT", httpRequestURL, null);
501 public void sendHttpGET(String httpRequestURL) {
502 sendHttpRequest("GET", httpRequestURL, null);
505 public int getPortFromShortenedUrl(String httpRequestURL) {
506 if (httpRequestURL.startsWith(":")) {
507 int end = httpRequestURL.indexOf("/");
508 return Integer.parseInt(httpRequestURL.substring(1, end));
510 return cameraConfig.getPort();
513 public String getTinyUrl(String httpRequestURL) {
514 if (httpRequestURL.startsWith(":")) {
515 int beginIndex = httpRequestURL.indexOf("/");
516 return httpRequestURL.substring(beginIndex);
518 return httpRequestURL;
521 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
522 // The authHandler will generate a digest string and re-send using this same function when needed.
523 @SuppressWarnings("null")
524 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
525 int port = getPortFromShortenedUrl(httpRequestURLFull);
526 String httpRequestURL = getTinyUrl(httpRequestURLFull);
528 if (mainBootstrap == null) {
529 mainBootstrap = new Bootstrap();
530 mainBootstrap.group(mainEventLoopGroup);
531 mainBootstrap.channel(NioSocketChannel.class);
532 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
533 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
534 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
535 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
536 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
537 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
540 public void initChannel(SocketChannel socketChannel) throws Exception {
541 // HIK Alarm stream needs > 9sec idle to stop stream closing
542 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
543 socketChannel.pipeline().addLast(new HttpClientCodec());
544 socketChannel.pipeline().addLast(AUTH_HANDLER,
545 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
546 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
548 switch (thing.getThingTypeUID().getId()) {
550 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
553 socketChannel.pipeline()
554 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
557 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
560 socketChannel.pipeline().addLast(
561 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
563 case HIKVISION_THING:
564 socketChannel.pipeline()
565 .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
568 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
571 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
578 FullHttpRequest request;
579 if (!"PUT".equals(httpMethod) || (useDigestAuth && digestString == null)) {
580 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
581 request.headers().set("Host", cameraConfig.getIp() + ":" + port);
582 request.headers().set("Connection", HttpHeaderValues.KEEP_ALIVE);
584 request = putRequestWithBody;
587 if (!basicAuth.isEmpty()) {
589 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
592 request.headers().set("Authorization", "Basic " + basicAuth);
597 if (digestString != null) {
598 request.headers().set("Authorization", "Digest " + digestString);
602 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
603 .addListener(new ChannelFutureListener() {
606 public void operationComplete(@Nullable ChannelFuture future) {
607 if (future == null) {
610 if (future.isDone() && future.isSuccess()) {
611 Channel ch = future.channel();
612 openChannels.add(ch);
616 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
619 openChannel(ch, httpRequestURL);
620 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
621 commonHandler.setURL(httpRequestURLFull);
622 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
623 authHandler.setURL(httpMethod, httpRequestURL);
625 switch (thing.getThingTypeUID().getId()) {
627 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
628 amcrestHandler.setURL(httpRequestURL);
631 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
632 instarHandler.setURL(httpRequestURL);
635 ch.writeAndFlush(request);
636 } else { // an error occured
637 cameraCommunicationError(
638 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
644 public void processSnapshot(byte[] incommingSnapshot) {
645 lockCurrentSnapshot.lock();
647 currentSnapshot = incommingSnapshot;
648 if (cameraConfig.getGifPreroll() > 0) {
649 fifoSnapshotBuffer.add(incommingSnapshot);
650 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
651 fifoSnapshotBuffer.removeFirst();
655 lockCurrentSnapshot.unlock();
658 if (streamingSnapshotMjpeg) {
659 sendMjpegFrame(incommingSnapshot, snapshotMjpegChannelGroup);
661 if (streamingAutoFps) {
662 if (motionDetected) {
663 sendMjpegFrame(incommingSnapshot, autoSnapshotMjpegChannelGroup);
664 } else if (updateAutoFps) {
665 // only happens every 8 seconds as some browsers need a frame that often to keep stream alive.
666 sendMjpegFrame(incommingSnapshot, autoSnapshotMjpegChannelGroup);
667 updateAutoFps = false;
671 if (updateImageChannel) {
672 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
673 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
674 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
675 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
676 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
677 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
678 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
682 public void stopStreamServer() {
683 serversLoopGroup.shutdownGracefully();
684 serverBootstrap = null;
687 @SuppressWarnings("null")
688 public void startStreamServer() {
689 if (serverBootstrap == null) {
691 serversLoopGroup = new NioEventLoopGroup();
692 serverBootstrap = new ServerBootstrap();
693 serverBootstrap.group(serversLoopGroup);
694 serverBootstrap.channel(NioServerSocketChannel.class);
695 // IP "0.0.0.0" will bind the server to all network connections//
696 serverBootstrap.localAddress(new InetSocketAddress("0.0.0.0", cameraConfig.getServerPort()));
697 serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
699 protected void initChannel(SocketChannel socketChannel) throws Exception {
700 socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 60, 0));
701 socketChannel.pipeline().addLast("HttpServerCodec", new HttpServerCodec());
702 socketChannel.pipeline().addLast("ChunkedWriteHandler", new ChunkedWriteHandler());
703 socketChannel.pipeline().addLast("streamServerHandler", new StreamServerHandler(getHandle()));
706 serverFuture = serverBootstrap.bind().sync();
707 serverFuture.await(4000);
708 logger.debug("File server for camera at {} has started on port {} for all NIC's.", cameraConfig.getIp(),
709 cameraConfig.getServerPort());
710 updateState(CHANNEL_MJPEG_URL,
711 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.mjpeg"));
712 updateState(CHANNEL_HLS_URL,
713 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.m3u8"));
714 updateState(CHANNEL_IMAGE_URL,
715 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.jpg"));
716 } catch (Exception e) {
717 cameraConfigError("Exception when starting server. Try changing the Server Port to another number.");
719 if (thing.getThingTypeUID().getId().equals(INSTAR_THING)) {
720 logger.debug("Setting up the Alarm Server settings in the camera now");
722 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
723 + hostIp + "&-as_port=" + cameraConfig.getServerPort()
724 + "&-as_path=/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");
729 public void setupSnapshotStreaming(boolean stream, ChannelHandlerContext ctx, boolean auto) {
731 sendMjpegFirstPacket(ctx);
733 autoSnapshotMjpegChannelGroup.add(ctx.channel());
734 lockCurrentSnapshot.lock();
736 sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
737 // iOS uses a FIFO? and needs two frames to display a pic
738 sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
740 lockCurrentSnapshot.unlock();
742 streamingAutoFps = true;
744 snapshotMjpegChannelGroup.add(ctx.channel());
745 lockCurrentSnapshot.lock();
747 sendMjpegFrame(currentSnapshot, snapshotMjpegChannelGroup);
749 lockCurrentSnapshot.unlock();
751 streamingSnapshotMjpeg = true;
752 startSnapshotPolling();
755 snapshotMjpegChannelGroup.remove(ctx.channel());
756 autoSnapshotMjpegChannelGroup.remove(ctx.channel());
757 if (streamingSnapshotMjpeg && snapshotMjpegChannelGroup.isEmpty()) {
758 streamingSnapshotMjpeg = false;
759 stopSnapshotPolling();
760 logger.debug("All snapshots.mjpeg streams have stopped.");
761 } else if (streamingAutoFps && autoSnapshotMjpegChannelGroup.isEmpty()) {
762 streamingAutoFps = false;
763 stopSnapshotPolling();
764 logger.debug("All autofps.mjpeg streams have stopped.");
769 // If start is true the CTX is added to the list to stream video to, false stops
771 public void setupMjpegStreaming(boolean start, ChannelHandlerContext ctx) {
773 if (mjpegChannelGroup.isEmpty()) {// first stream being requested.
774 mjpegChannelGroup.add(ctx.channel());
775 if (mjpegUri.isEmpty() || mjpegUri.equals("ffmpeg")) {
776 sendMjpegFirstPacket(ctx);
777 setupFfmpegFormat(FFmpegFormat.MJPEG);
780 // fix Dahua reboots when refreshing a mjpeg stream.
781 TimeUnit.MILLISECONDS.sleep(500);
782 } catch (InterruptedException e) {
784 sendHttpGET(mjpegUri);
786 } else if (ffmpegMjpeg != null) {// not first stream and we will use ffmpeg
787 sendMjpegFirstPacket(ctx);
788 mjpegChannelGroup.add(ctx.channel());
789 } else {// not first stream and camera supplies the mjpeg source.
790 ctx.channel().writeAndFlush(firstStreamedMsg);
791 mjpegChannelGroup.add(ctx.channel());
794 mjpegChannelGroup.remove(ctx.channel());
795 if (mjpegChannelGroup.isEmpty()) {
796 logger.debug("All ipcamera.mjpeg streams have stopped.");
797 if (mjpegUri.equals("ffmpeg") || mjpegUri.isEmpty()) {
798 Ffmpeg localMjpeg = ffmpegMjpeg;
799 if (localMjpeg != null) {
800 localMjpeg.stopConverting();
803 closeChannel(getTinyUrl(mjpegUri));
809 void openChannel(Channel channel, String httpRequestURL) {
810 ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
811 if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
812 tracker.setChannel(channel);
815 channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
818 void closeChannel(String url) {
819 ChannelTracking channelTracking = channelTrackingMap.get(url);
820 if (channelTracking != null) {
821 if (channelTracking.getChannel().isOpen()) {
822 channelTracking.getChannel().close();
829 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
830 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
833 void cleanChannels() {
834 for (Channel channel : openChannels) {
835 boolean oldChannel = true;
836 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
837 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
838 channelTrackingMap.remove(channelTracking.getRequestUrl());
840 if (channelTracking.getChannel() == channel) {
841 logger.trace("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
851 public void storeHttpReply(String url, String content) {
852 ChannelTracking channelTracking = channelTrackingMap.get(url);
853 if (channelTracking != null) {
854 channelTracking.setReply(content);
858 // sends direct to ctx so can be either snapshots.mjpeg or normal mjpeg stream
859 public void sendMjpegFirstPacket(ChannelHandlerContext ctx) {
860 final String boundary = "thisMjpegStream";
861 String contentType = "multipart/x-mixed-replace; boundary=" + boundary;
862 HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
863 response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
864 response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
865 response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
866 response.headers().add("Access-Control-Allow-Origin", "*");
867 response.headers().add("Access-Control-Expose-Headers", "*");
868 ctx.channel().writeAndFlush(response);
871 public void sendMjpegFrame(byte[] jpg, ChannelGroup channelGroup) {
872 final String boundary = "thisMjpegStream";
873 ByteBuf imageByteBuf = Unpooled.copiedBuffer(jpg);
874 int length = imageByteBuf.readableBytes();
875 String header = "--" + boundary + "\r\n" + "content-type: image/jpeg" + "\r\n" + "content-length: " + length
877 ByteBuf headerBbuf = Unpooled.copiedBuffer(header, 0, header.length(), StandardCharsets.UTF_8);
878 ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
879 streamToGroup(headerBbuf, channelGroup, false);
880 streamToGroup(imageByteBuf, channelGroup, false);
881 streamToGroup(footerBbuf, channelGroup, true);
884 public void streamToGroup(Object msg, ChannelGroup channelGroup, boolean flush) {
885 channelGroup.write(msg);
887 channelGroup.flush();
891 private void storeSnapshots() {
893 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
894 lockCurrentSnapshot.lock();
896 for (byte[] foo : fifoSnapshotBuffer) {
897 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
900 OutputStream fos = new FileOutputStream(file);
903 } catch (FileNotFoundException e) {
904 logger.warn("FileNotFoundException {}", e.getMessage());
905 } catch (IOException e) {
906 logger.warn("IOException {}", e.getMessage());
910 lockCurrentSnapshot.unlock();
914 public void setupFfmpegFormat(FFmpegFormat format) {
915 String inputOptions = cameraConfig.getFfmpegInputOptions();
916 if (cameraConfig.getFfmpegOutput().isEmpty()) {
917 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
920 if (rtspUri.isEmpty()) {
921 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
924 if (cameraConfig.getFfmpegLocation().isEmpty()) {
925 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
928 if (rtspUri.toLowerCase().contains("rtsp")) {
929 if (inputOptions.isEmpty()) {
930 inputOptions = "-rtsp_transport tcp";
934 // Make sure the folder exists, if not create it.
935 new File(cameraConfig.getFfmpegOutput()).mkdirs();
938 if (ffmpegHLS == null) {
939 if (!inputOptions.isEmpty()) {
940 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
941 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
942 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
943 cameraConfig.getUser(), cameraConfig.getPassword());
945 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
946 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
947 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
948 cameraConfig.getPassword());
951 Ffmpeg localHLS = ffmpegHLS;
952 if (localHLS != null) {
953 localHLS.startConverting();
957 if (cameraConfig.getGifPreroll() > 0) {
958 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
959 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
960 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
961 + cameraConfig.getGifOutOptions(),
962 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
963 cameraConfig.getPassword());
965 if (!inputOptions.isEmpty()) {
966 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
968 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
970 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
971 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
972 cameraConfig.getUser(), cameraConfig.getPassword());
974 if (cameraConfig.getGifPreroll() > 0) {
977 Ffmpeg localGIF = ffmpegGIF;
978 if (localGIF != null) {
979 localGIF.startConverting();
980 if (gifHistory.isEmpty()) {
981 gifHistory = gifFilename;
982 } else if (!gifFilename.equals("ipcamera")) {
983 gifHistory = gifFilename + "," + gifHistory;
984 if (gifHistoryLength > 49) {
985 int endIndex = gifHistory.lastIndexOf(",");
986 gifHistory = gifHistory.substring(0, endIndex);
989 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
993 if (!inputOptions.isEmpty()) {
994 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
996 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
998 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
999 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
1000 cameraConfig.getUser(), cameraConfig.getPassword());
1001 Ffmpeg localRecord = ffmpegRecord;
1002 if (localRecord != null) {
1003 localRecord.startConverting();
1004 if (mp4History.isEmpty()) {
1005 mp4History = mp4Filename;
1006 } else if (!mp4Filename.equals("ipcamera")) {
1007 mp4History = mp4Filename + "," + mp4History;
1008 if (mp4HistoryLength > 49) {
1009 int endIndex = mp4History.lastIndexOf(",");
1010 mp4History = mp4History.substring(0, endIndex);
1014 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1017 Ffmpeg localAlarms = ffmpegRtspHelper;
1018 if (localAlarms != null) {
1019 localAlarms.stopConverting();
1020 if (!audioAlarmEnabled && !motionAlarmEnabled) {
1024 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
1025 String filterOptions = "";
1026 if (!audioAlarmEnabled) {
1027 filterOptions = "-an";
1029 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
1031 if (!motionAlarmEnabled && !ffmpegSnapshotGeneration) {
1032 filterOptions = filterOptions.concat(" -vn");
1033 } else if (motionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
1034 String usersMotionOptions = cameraConfig.getMotionOptions();
1035 if (usersMotionOptions.startsWith("-")) {
1036 // Need to put the users custom options first in the chain before the motion is detected
1037 filterOptions += " " + usersMotionOptions + ",select='gte(scene," + motionThreshold
1038 + ")',metadata=print";
1040 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
1041 + motionThreshold + ")',metadata=print";
1043 } else if (motionAlarmEnabled) {
1044 filterOptions = filterOptions
1045 .concat(" -vf select='gte(scene," + motionThreshold + ")',metadata=print");
1047 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
1048 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
1049 localAlarms = ffmpegRtspHelper;
1050 if (localAlarms != null) {
1051 localAlarms.startConverting();
1055 if (ffmpegMjpeg == null) {
1056 if (inputOptions.isEmpty()) {
1057 inputOptions = "-hide_banner -loglevel warning";
1059 inputOptions += " -hide_banner -loglevel warning";
1061 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1062 cameraConfig.getMjpegOptions(),
1063 "http://127.0.0.1:" + cameraConfig.getServerPort() + "/ipcamera.jpg",
1064 cameraConfig.getUser(), cameraConfig.getPassword());
1066 Ffmpeg localMjpeg = ffmpegMjpeg;
1067 if (localMjpeg != null) {
1068 localMjpeg.startConverting();
1072 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
1073 if (ffmpegSnapshot == null) {
1074 if (inputOptions.isEmpty()) {
1076 inputOptions = "-threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1078 inputOptions += " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1080 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1081 cameraConfig.getSnapshotOptions(),
1082 "http://127.0.0.1:" + cameraConfig.getServerPort() + "/snapshot.jpg",
1083 cameraConfig.getUser(), cameraConfig.getPassword());
1085 Ffmpeg localSnaps = ffmpegSnapshot;
1086 if (localSnaps != null) {
1087 localSnaps.startConverting();
1093 public void noMotionDetected(String thisAlarmsChannel) {
1094 setChannelState(thisAlarmsChannel, OnOffType.OFF);
1095 firstMotionAlarm = false;
1096 motionAlarmUpdateSnapshot = false;
1097 motionDetected = false;
1098 if (streamingAutoFps) {
1099 stopSnapshotPolling();
1100 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1101 stopSnapshotPolling();
1106 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
1107 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
1108 * tampering with the camera.
1110 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
1111 updateState(thisAlarmsChannel, state);
1114 public void motionDetected(String thisAlarmsChannel) {
1115 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
1116 updateState(thisAlarmsChannel, OnOffType.ON);
1117 motionDetected = true;
1118 if (streamingAutoFps) {
1119 startSnapshotPolling();
1121 if (cameraConfig.getUpdateImageWhen().contains("2")) {
1122 if (!firstMotionAlarm) {
1123 if (!snapshotUri.isEmpty()) {
1124 sendHttpGET(snapshotUri);
1126 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1128 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1129 if (!snapshotPolling) {
1130 startSnapshotPolling();
1132 firstMotionAlarm = true;
1133 motionAlarmUpdateSnapshot = true;
1137 public void audioDetected() {
1138 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1139 if (cameraConfig.getUpdateImageWhen().contains("3")) {
1140 if (!firstAudioAlarm) {
1141 if (!snapshotUri.isEmpty()) {
1142 sendHttpGET(snapshotUri);
1144 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1146 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1147 firstAudioAlarm = true;
1148 audioAlarmUpdateSnapshot = true;
1152 public void noAudioDetected() {
1153 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1154 firstAudioAlarm = false;
1155 audioAlarmUpdateSnapshot = false;
1158 public void recordMp4(String filename, int seconds) {
1159 mp4Filename = filename;
1160 mp4RecordTime = seconds;
1161 setupFfmpegFormat(FFmpegFormat.RECORD);
1162 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1165 public void recordGif(String filename, int seconds) {
1166 gifFilename = filename;
1167 gifRecordTime = seconds;
1168 if (cameraConfig.getGifPreroll() > 0) {
1169 snapCount = seconds;
1171 setupFfmpegFormat(FFmpegFormat.GIF);
1173 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1176 public String returnValueFromString(String rawString, String searchedString) {
1178 int index = rawString.indexOf(searchedString);
1179 if (index != -1) // -1 means "not found"
1181 result = rawString.substring(index + searchedString.length(), rawString.length());
1182 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1184 return result; // Did not find a carriage return.
1186 return result.substring(0, index);
1189 return ""; // Did not find the String we were searching for
1192 private void sendPTZRequest() {
1193 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1197 public void handleCommand(ChannelUID channelUID, Command command) {
1198 if (command instanceof RefreshType) {
1199 switch (channelUID.getId()) {
1201 if (onvifCamera.supportsPTZ()) {
1202 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1206 if (onvifCamera.supportsPTZ()) {
1207 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1211 if (onvifCamera.supportsPTZ()) {
1212 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1215 case CHANNEL_GOTO_PRESET:
1216 if (onvifCamera.supportsPTZ()) {
1217 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1221 } // caution "REFRESH" can still progress to brand Handlers below the else.
1223 switch (channelUID.getId()) {
1224 case CHANNEL_MP4_HISTORY_LENGTH:
1225 if (DecimalType.ZERO.equals(command)) {
1226 mp4HistoryLength = 0;
1228 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1231 case CHANNEL_GIF_HISTORY_LENGTH:
1232 if (DecimalType.ZERO.equals(command)) {
1233 gifHistoryLength = 0;
1235 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1238 case CHANNEL_FFMPEG_MOTION_CONTROL:
1239 if (OnOffType.ON.equals(command)) {
1240 motionAlarmEnabled = true;
1241 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1242 motionAlarmEnabled = false;
1243 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1245 motionAlarmEnabled = true;
1246 motionThreshold = Double.valueOf(command.toString());
1247 motionThreshold = motionThreshold / 10000;
1249 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1251 case CHANNEL_START_STREAM:
1253 if (OnOffType.ON.equals(command)) {
1254 localHLS = ffmpegHLS;
1255 if (localHLS == null) {
1256 setupFfmpegFormat(FFmpegFormat.HLS);
1257 localHLS = ffmpegHLS;
1259 if (localHLS != null) {
1260 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1261 localHLS.startConverting();
1264 localHLS = ffmpegHLS;
1265 if (localHLS != null) {
1266 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1267 localHLS.setKeepAlive(1);
1271 case CHANNEL_EXTERNAL_MOTION:
1272 if (OnOffType.ON.equals(command)) {
1273 motionDetected(CHANNEL_EXTERNAL_MOTION);
1275 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1278 case CHANNEL_GOTO_PRESET:
1279 if (onvifCamera.supportsPTZ()) {
1280 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1283 case CHANNEL_POLL_IMAGE:
1284 if (OnOffType.ON.equals(command)) {
1285 if (snapshotUri.isEmpty()) {
1286 ffmpegSnapshotGeneration = true;
1287 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1288 updateImageChannel = false;
1290 updateImageChannel = true;
1291 sendHttpGET(snapshotUri);// Allows this to change Image FPS on demand
1294 Ffmpeg localSnaps = ffmpegSnapshot;
1295 if (localSnaps != null) {
1296 localSnaps.stopConverting();
1297 ffmpegSnapshotGeneration = false;
1299 updateImageChannel = false;
1303 if (onvifCamera.supportsPTZ()) {
1304 if (command instanceof IncreaseDecreaseType) {
1305 if (command == IncreaseDecreaseType.INCREASE) {
1306 if (cameraConfig.getPtzContinuous()) {
1307 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1309 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1312 if (cameraConfig.getPtzContinuous()) {
1313 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1315 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1319 } else if (OnOffType.OFF.equals(command)) {
1320 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1323 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1324 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1328 if (onvifCamera.supportsPTZ()) {
1329 if (command instanceof IncreaseDecreaseType) {
1330 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1331 if (cameraConfig.getPtzContinuous()) {
1332 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1334 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1337 if (cameraConfig.getPtzContinuous()) {
1338 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1340 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1344 } else if (OnOffType.OFF.equals(command)) {
1345 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1348 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1349 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1353 if (onvifCamera.supportsPTZ()) {
1354 if (command instanceof IncreaseDecreaseType) {
1355 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1356 if (cameraConfig.getPtzContinuous()) {
1357 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1359 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1362 if (cameraConfig.getPtzContinuous()) {
1363 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1365 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1369 } else if (OnOffType.OFF.equals(command)) {
1370 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1373 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1374 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1379 // commands and refresh now get passed to brand handlers
1380 switch (thing.getThingTypeUID().getId()) {
1382 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1383 amcrestHandler.handleCommand(channelUID, command);
1384 if (lowPriorityRequests.isEmpty()) {
1385 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1389 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1390 dahuaHandler.handleCommand(channelUID, command);
1391 if (lowPriorityRequests.isEmpty()) {
1392 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1395 case DOORBIRD_THING:
1396 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1397 doorBirdHandler.handleCommand(channelUID, command);
1398 if (lowPriorityRequests.isEmpty()) {
1399 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1402 case HIKVISION_THING:
1403 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1404 hikvisionHandler.handleCommand(channelUID, command);
1405 if (lowPriorityRequests.isEmpty()) {
1406 lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1410 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1411 cameraConfig.getPassword());
1412 foscamHandler.handleCommand(channelUID, command);
1413 if (lowPriorityRequests.isEmpty()) {
1414 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1418 InstarHandler instarHandler = new InstarHandler(getHandle());
1419 instarHandler.handleCommand(channelUID, command);
1420 if (lowPriorityRequests.isEmpty()) {
1421 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1425 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1426 defaultHandler.handleCommand(channelUID, command);
1427 if (lowPriorityRequests.isEmpty()) {
1428 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1434 public void setChannelState(String channelToUpdate, State valueOf) {
1435 updateState(channelToUpdate, valueOf);
1438 private void bringCameraOnline() {
1440 updateStatus(ThingStatus.ONLINE);
1441 groupTracker.listOfOnlineCameraHandlers.add(this);
1442 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1443 Future<?> localFuture = cameraConnectionJob;
1444 if (localFuture != null) {
1445 localFuture.cancel(false);
1448 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1449 snapshotPolling = true;
1450 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000, cameraConfig.getPollTime(),
1451 TimeUnit.MILLISECONDS);
1454 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1456 if (!rtspUri.isEmpty()) {
1457 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1459 if (updateImageChannel) {
1460 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1462 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1464 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1465 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1466 handle.cameraOnline(getThing().getUID().getId());
1471 void snapshotIsFfmpeg() {
1472 bringCameraOnline();
1473 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1475 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1476 if (!rtspUri.isEmpty()) {
1477 updateImageChannel = false;
1478 ffmpegSnapshotGeneration = true;
1479 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1480 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1482 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1486 void pollingCameraConnection() {
1487 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1488 if (rtspUri.isEmpty()) {
1489 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1491 if (snapshotUri.isEmpty() || snapshotUri.equals("ffmpeg")) {
1494 sendHttpRequest("GET", snapshotUri, null);
1498 if (!onvifCamera.isConnected()) {
1499 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1500 cameraConfig.getOnvifPort());
1501 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1503 if (snapshotUri.equals("ffmpeg")) {
1505 } else if (!snapshotUri.isEmpty()) {
1506 sendHttpRequest("GET", snapshotUri, null);
1507 } else if (!rtspUri.isEmpty()) {
1510 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1511 "Camera failed to report a valid Snaphot and/or RTSP URL. See readme on how to use the SNAPSHOT_URL_OVERRIDE feature.");
1515 public void cameraConfigError(String reason) {
1516 // wont try to reconnect again due to a config error being the cause.
1517 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1521 public void cameraCommunicationError(String reason) {
1522 // will try to reconnect again as camera may be rebooting.
1523 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1524 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1525 resetAndRetryConnecting();
1529 boolean streamIsStopped(String url) {
1530 ChannelTracking channelTracking = channelTrackingMap.get(url);
1531 if (channelTracking != null) {
1532 if (channelTracking.getChannel().isActive()) {
1533 return false; // stream is running.
1536 return true; // Stream stopped or never started.
1539 void snapshotRunnable() {
1540 // Snapshot should be first to keep consistent time between shots
1541 sendHttpGET(snapshotUri);
1542 if (snapCount > 0) {
1543 if (--snapCount == 0) {
1544 setupFfmpegFormat(FFmpegFormat.GIF);
1549 public void stopSnapshotPolling() {
1550 Future<?> localFuture;
1551 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1552 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1553 snapshotPolling = false;
1554 localFuture = snapshotJob;
1555 if (localFuture != null) {
1556 localFuture.cancel(true);
1558 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1559 snapshotPolling = false;
1560 localFuture = snapshotJob;
1561 if (localFuture != null) {
1562 localFuture.cancel(true);
1567 public void startSnapshotPolling() {
1568 if (snapshotPolling || ffmpegSnapshotGeneration) {
1569 return; // Already polling or creating with FFmpeg from RTSP
1571 if (streamingSnapshotMjpeg || streamingAutoFps) {
1572 snapshotPolling = true;
1573 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1574 TimeUnit.MILLISECONDS);
1575 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1576 snapshotPolling = true;
1577 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1578 TimeUnit.MILLISECONDS);
1583 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep mjpeg and alarm
1584 * streams open and more.
1587 void pollCameraRunnable() {
1588 // Snapshot should be first to keep consistent time between shots
1589 if (streamingAutoFps) {
1590 updateAutoFps = true;
1591 if (!snapshotPolling && !ffmpegSnapshotGeneration) {
1592 // Dont need to poll if creating from RTSP stream with FFmpeg or we are polling at full rate already.
1593 sendHttpGET(snapshotUri);
1595 } else if (!snapshotUri.isEmpty() && !snapshotPolling) {// we need to check camera is still online.
1596 sendHttpGET(snapshotUri);
1598 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1599 if (!lowPriorityRequests.isEmpty()) {
1600 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1601 lowPriorityCounter = 0;
1603 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1605 // what needs to be done every poll//
1606 switch (thing.getThingTypeUID().getId()) {
1610 if (!onvifCamera.isConnected()) {
1611 onvifCamera.connect(true);
1615 noMotionDetected(CHANNEL_MOTION_ALARM);
1616 noMotionDetected(CHANNEL_PIR_ALARM);
1619 case HIKVISION_THING:
1620 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1621 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1622 cameraConfig.getIp());
1623 sendHttpGET("/ISAPI/Event/notification/alertStream");
1627 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1628 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1631 // Check for alarms, channel for NVRs appears not to work at filtering.
1632 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1633 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1634 cameraConfig.getIp());
1635 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1638 case DOORBIRD_THING:
1639 // Check for alarms, channel for NVRs appears not to work at filtering.
1640 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1641 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1642 cameraConfig.getIp());
1643 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1647 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1648 + cameraConfig.getPassword());
1651 Ffmpeg localHLS = ffmpegHLS;
1652 if (localHLS != null) {
1653 localHLS.checkKeepAlive();
1655 if (openChannels.size() > 18) {
1656 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1662 public void initialize() {
1663 cameraConfig = getConfigAs(CameraConfig.class);
1664 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1665 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1666 rtspUri = cameraConfig.getFfmpegInput();
1668 if (cameraConfig.getServerPort() < 1) {
1670 "The Server Port is not set to a valid number which disables a lot of binding features. See readme for more info.");
1671 } else if (cameraConfig.getServerPort() < 1025) {
1672 logger.warn("The Server Port is <= 1024 and may cause permission errors under Linux, try a higher number.");
1675 // Known cameras will connect quicker if we skip ONVIF questions.
1676 switch (thing.getThingTypeUID().getId()) {
1679 if (mjpegUri.isEmpty()) {
1680 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1682 if (snapshotUri.isEmpty()) {
1683 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1686 case DOORBIRD_THING:
1687 if (mjpegUri.isEmpty()) {
1688 mjpegUri = "/bha-api/video.cgi";
1690 if (snapshotUri.isEmpty()) {
1691 snapshotUri = "/bha-api/image.cgi";
1695 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1696 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1697 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1698 if (mjpegUri.isEmpty()) {
1699 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1700 + cameraConfig.getPassword();
1702 if (snapshotUri.isEmpty()) {
1703 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1704 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1707 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1708 if (mjpegUri.isEmpty()) {
1709 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1711 if (snapshotUri.isEmpty()) {
1712 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1716 if (snapshotUri.isEmpty()) {
1717 snapshotUri = "/tmpfs/snap.jpg";
1719 if (mjpegUri.isEmpty()) {
1720 mjpegUri = "/mjpegstream.cgi?-chn=12";
1725 // Onvif and Instar event handling needs the host IP and the server started.
1726 if (cameraConfig.getServerPort() > 0) {
1727 startStreamServer();
1730 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1731 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1732 cameraConfig.getUser(), cameraConfig.getPassword());
1733 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1734 // Only use ONVIF events if it is not an API camera.
1735 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1738 // for poll times above 9 seconds don't display a warning about the Image channel.
1739 if (9000 <= cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1741 "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.");
1743 // Waiting 3 seconds for ONVIF to discover the urls before running.
1744 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 30, TimeUnit.SECONDS);
1747 // What the camera needs to re-connect if the initialize() is not called.
1748 private void resetAndRetryConnecting() {
1754 public void dispose() {
1756 snapshotPolling = false;
1757 onvifCamera.disconnect();
1758 Future<?> localFuture = pollCameraJob;
1759 if (localFuture != null) {
1760 localFuture.cancel(true);
1762 localFuture = snapshotJob;
1763 if (localFuture != null) {
1764 localFuture.cancel(true);
1766 localFuture = cameraConnectionJob;
1767 if (localFuture != null) {
1768 localFuture.cancel(true);
1770 threadPool.shutdown();
1771 threadPool = Executors.newScheduledThreadPool(4);
1773 groupTracker.listOfOnlineCameraHandlers.remove(this);
1774 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1775 // inform all group handlers that this camera has gone offline
1776 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1777 handle.cameraOffline(this);
1779 basicAuth = ""; // clear out stored Password hash
1780 useDigestAuth = false;
1782 openChannels.close();
1784 Ffmpeg localFfmpeg = ffmpegHLS;
1785 if (localFfmpeg != null) {
1786 localFfmpeg.stopConverting();
1789 localFfmpeg = ffmpegRecord;
1790 if (localFfmpeg != null) {
1791 localFfmpeg.stopConverting();
1793 localFfmpeg = ffmpegGIF;
1794 if (localFfmpeg != null) {
1795 localFfmpeg.stopConverting();
1797 localFfmpeg = ffmpegRtspHelper;
1798 if (localFfmpeg != null) {
1799 localFfmpeg.stopConverting();
1801 localFfmpeg = ffmpegMjpeg;
1802 if (localFfmpeg != null) {
1803 localFfmpeg.stopConverting();
1805 localFfmpeg = ffmpegSnapshot;
1806 if (localFfmpeg != null) {
1807 localFfmpeg.stopConverting();
1809 channelTrackingMap.clear();
1812 public void setStreamServerHandler(StreamServerHandler streamServerHandler2) {
1813 streamServerHandler = streamServerHandler2;
1816 public String getWhiteList() {
1817 return cameraConfig.getIpWhitelist();
1821 public Collection<Class<? extends ThingHandlerService>> getServices() {
1822 return Collections.singleton(IpCameraActions.class);