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 Object reply = new Object();
210 private String requestUrl = "";
211 private boolean closeConnection = true;
212 private boolean isChunked = false;
214 public void setURL(String url) {
219 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
220 if (msg == null || ctx == null) {
224 if (msg instanceof HttpResponse) {
225 HttpResponse response = (HttpResponse) msg;
226 if (response.status().code() != 401) {
227 if (!response.headers().isEmpty()) {
228 for (String name : response.headers().names()) {
229 // Some cameras use first letter uppercase and others dont.
230 switch (name.toLowerCase()) { // Possible localization issues doing this
232 contentType = response.headers().getAsString(name);
234 case "content-length":
235 bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
238 if (response.headers().getAsString(name).contains("keep-alive")) {
239 closeConnection = false;
242 case "transfer-encoding":
243 if (response.headers().getAsString(name).contains("chunked")) {
249 if (contentType.contains("multipart")) {
250 closeConnection = false;
251 if (mjpegUri.equals(requestUrl)) {
252 if (msg instanceof HttpMessage) {
253 // very start of stream only
254 ReferenceCountUtil.retain(msg, 1);
255 firstStreamedMsg = msg;
256 streamToGroup(firstStreamedMsg, mjpegChannelGroup, true);
259 } else if (contentType.contains("image/jp")) {
260 if (bytesToRecieve == 0) {
261 bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
262 logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
264 incomingJpeg = new byte[bytesToRecieve];
269 if (msg instanceof HttpContent) {
270 if (mjpegUri.equals(requestUrl)) {
271 // multiple MJPEG stream packets come back as this.
272 ReferenceCountUtil.retain(msg, 1);
273 streamToGroup(msg, mjpegChannelGroup, true);
275 HttpContent content = (HttpContent) msg;
276 // Found some cameras use Content-Type: image/jpg instead of image/jpeg
277 if (contentType.contains("image/jp")) {
278 for (int i = 0; i < content.content().capacity(); i++) {
279 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
281 if (content instanceof LastHttpContent) {
282 processSnapshot(incomingJpeg);
283 // testing next line and if works need to do a full cleanup of this function.
284 closeConnection = true;
285 if (closeConnection) {
289 bytesAlreadyRecieved = 0;
292 } else { // incomingMessage that is not an IMAGE
293 if (incomingMessage.isEmpty()) {
294 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
296 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
298 bytesAlreadyRecieved = incomingMessage.length();
299 if (content instanceof LastHttpContent) {
300 // If it is not an image send it on to the next handler//
301 if (bytesAlreadyRecieved != 0) {
302 reply = incomingMessage;
303 super.channelRead(ctx, reply);
306 // Alarm Streams never have a LastHttpContent as they always stay open//
307 else if (contentType.contains("multipart")) {
308 if (bytesAlreadyRecieved != 0) {
309 reply = incomingMessage;
310 incomingMessage = "";
312 bytesAlreadyRecieved = 0;
313 super.channelRead(ctx, reply);
316 // Foscam needs this as will other cameras with chunks//
317 if (isChunked && bytesAlreadyRecieved != 0) {
318 logger.debug("Reply is chunked.");
319 reply = incomingMessage;
320 super.channelRead(ctx, reply);
324 } else { // msg is not HttpContent
325 // Foscam cameras need this
326 if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
327 reply = incomingMessage;
328 logger.debug("Packet back from camera is {}", incomingMessage);
329 super.channelRead(ctx, reply);
333 ReferenceCountUtil.release(msg);
338 public void channelReadComplete(@Nullable ChannelHandlerContext ctx) {
342 public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
346 public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
350 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
351 if (cause == null || ctx == null) {
354 if (cause instanceof ArrayIndexOutOfBoundsException) {
355 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
358 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
365 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
369 if (evt instanceof IdleStateEvent) {
370 IdleStateEvent e = (IdleStateEvent) evt;
371 // If camera does not use the channel for X amount of time it will close.
372 if (e.state() == IdleState.READER_IDLE) {
373 String urlToKeepOpen = "";
374 switch (thing.getThingTypeUID().getId()) {
376 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
378 case HIKVISION_THING:
379 urlToKeepOpen = "/ISAPI/Event/notification/alertStream";
382 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
385 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
386 if (channelTracking != null) {
387 if (channelTracking.getChannel() == ctx.channel()) {
388 return; // don't auto close this as it is for the alarms.
397 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
398 IpCameraDynamicStateDescriptionProvider stateDescriptionProvider) {
400 this.stateDescriptionProvider = stateDescriptionProvider;
401 if (ipAddress != null) {
404 hostIp = Helper.getLocalIpAddress();
406 this.groupTracker = groupTracker;
409 private IpCameraHandler getHandle() {
413 // false clears the stored user/pass hash, true creates the hash
414 public boolean setBasicAuth(boolean useBasic) {
416 logger.debug("Clearing out the stored BASIC auth now.");
419 } else if (!basicAuth.isEmpty()) {
420 // due to camera may have been sent multiple requests before the auth was set, this may trigger falsely.
421 logger.warn("Camera is reporting your username and/or password is wrong.");
424 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
425 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
426 ByteBuf byteBuf = null;
428 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
429 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
431 if (byteBuf != null) {
437 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
442 private String getCorrectUrlFormat(String longUrl) {
443 String temp = longUrl;
446 if (longUrl.isEmpty() || longUrl.equals("ffmpeg")) {
451 url = new URL(longUrl);
452 int port = url.getPort();
454 if (url.getQuery() == null) {
455 temp = url.getPath();
457 temp = url.getPath() + "?" + url.getQuery();
460 if (url.getQuery() == null) {
461 temp = ":" + url.getPort() + url.getPath();
463 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
466 } catch (MalformedURLException e) {
467 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
472 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
473 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
474 sendHttpRequest("PUT", httpRequestURL, null);
477 public void sendHttpGET(String httpRequestURL) {
478 sendHttpRequest("GET", httpRequestURL, null);
481 public int getPortFromShortenedUrl(String httpRequestURL) {
482 if (httpRequestURL.startsWith(":")) {
483 int end = httpRequestURL.indexOf("/");
484 return Integer.parseInt(httpRequestURL.substring(1, end));
486 return cameraConfig.getPort();
489 public String getTinyUrl(String httpRequestURL) {
490 if (httpRequestURL.startsWith(":")) {
491 int beginIndex = httpRequestURL.indexOf("/");
492 return httpRequestURL.substring(beginIndex);
494 return httpRequestURL;
497 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
498 // The authHandler will generate a digest string and re-send using this same function when needed.
499 @SuppressWarnings("null")
500 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
501 int port = getPortFromShortenedUrl(httpRequestURLFull);
502 String httpRequestURL = getTinyUrl(httpRequestURLFull);
504 if (mainBootstrap == null) {
505 mainBootstrap = new Bootstrap();
506 mainBootstrap.group(mainEventLoopGroup);
507 mainBootstrap.channel(NioSocketChannel.class);
508 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
509 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
510 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
511 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
512 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
513 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
516 public void initChannel(SocketChannel socketChannel) throws Exception {
517 // HIK Alarm stream needs > 9sec idle to stop stream closing
518 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
519 socketChannel.pipeline().addLast(new HttpClientCodec());
520 socketChannel.pipeline().addLast(AUTH_HANDLER,
521 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
522 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
524 switch (thing.getThingTypeUID().getId()) {
526 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
529 socketChannel.pipeline()
530 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
533 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
536 socketChannel.pipeline().addLast(
537 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
539 case HIKVISION_THING:
540 socketChannel.pipeline()
541 .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
544 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
547 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
554 FullHttpRequest request;
555 if (!"PUT".equals(httpMethod) || (useDigestAuth && digestString == null)) {
556 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
557 request.headers().set("Host", cameraConfig.getIp() + ":" + port);
558 request.headers().set("Connection", HttpHeaderValues.KEEP_ALIVE);
560 request = putRequestWithBody;
563 if (!basicAuth.isEmpty()) {
565 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
568 request.headers().set("Authorization", "Basic " + basicAuth);
573 if (digestString != null) {
574 request.headers().set("Authorization", "Digest " + digestString);
578 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
579 .addListener(new ChannelFutureListener() {
582 public void operationComplete(@Nullable ChannelFuture future) {
583 if (future == null) {
586 if (future.isDone() && future.isSuccess()) {
587 Channel ch = future.channel();
588 openChannels.add(ch);
592 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
595 openChannel(ch, httpRequestURL);
596 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
597 commonHandler.setURL(httpRequestURLFull);
598 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
599 authHandler.setURL(httpMethod, httpRequestURL);
601 switch (thing.getThingTypeUID().getId()) {
603 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
604 amcrestHandler.setURL(httpRequestURL);
607 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
608 instarHandler.setURL(httpRequestURL);
611 ch.writeAndFlush(request);
612 } else { // an error occured
613 cameraCommunicationError(
614 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
620 public void processSnapshot(byte[] incommingSnapshot) {
621 lockCurrentSnapshot.lock();
623 currentSnapshot = incommingSnapshot;
624 if (cameraConfig.getGifPreroll() > 0) {
625 fifoSnapshotBuffer.add(incommingSnapshot);
626 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
627 fifoSnapshotBuffer.removeFirst();
631 lockCurrentSnapshot.unlock();
634 if (streamingSnapshotMjpeg) {
635 sendMjpegFrame(incommingSnapshot, snapshotMjpegChannelGroup);
637 if (streamingAutoFps) {
638 if (motionDetected) {
639 sendMjpegFrame(incommingSnapshot, autoSnapshotMjpegChannelGroup);
640 } else if (updateAutoFps) {
641 // only happens every 8 seconds as some browsers need a frame that often to keep stream alive.
642 sendMjpegFrame(incommingSnapshot, autoSnapshotMjpegChannelGroup);
643 updateAutoFps = false;
647 if (updateImageChannel) {
648 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
649 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
650 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
651 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
652 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
653 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
654 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
658 public void stopStreamServer() {
659 serversLoopGroup.shutdownGracefully();
660 serverBootstrap = null;
663 @SuppressWarnings("null")
664 public void startStreamServer() {
665 if (serverBootstrap == null) {
667 serversLoopGroup = new NioEventLoopGroup();
668 serverBootstrap = new ServerBootstrap();
669 serverBootstrap.group(serversLoopGroup);
670 serverBootstrap.channel(NioServerSocketChannel.class);
671 // IP "0.0.0.0" will bind the server to all network connections//
672 serverBootstrap.localAddress(new InetSocketAddress("0.0.0.0", cameraConfig.getServerPort()));
673 serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
675 protected void initChannel(SocketChannel socketChannel) throws Exception {
676 socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 60, 0));
677 socketChannel.pipeline().addLast("HttpServerCodec", new HttpServerCodec());
678 socketChannel.pipeline().addLast("ChunkedWriteHandler", new ChunkedWriteHandler());
679 socketChannel.pipeline().addLast("streamServerHandler", new StreamServerHandler(getHandle()));
682 serverFuture = serverBootstrap.bind().sync();
683 serverFuture.await(4000);
684 logger.debug("File server for camera at {} has started on port {} for all NIC's.", cameraConfig.getIp(),
685 cameraConfig.getServerPort());
686 updateState(CHANNEL_MJPEG_URL,
687 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.mjpeg"));
688 updateState(CHANNEL_HLS_URL,
689 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.m3u8"));
690 updateState(CHANNEL_IMAGE_URL,
691 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.jpg"));
692 } catch (Exception e) {
693 cameraConfigError("Exception when starting server. Try changing the Server Port to another number.");
695 if (thing.getThingTypeUID().getId().equals(INSTAR_THING)) {
696 logger.debug("Setting up the Alarm Server settings in the camera now");
698 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
699 + hostIp + "&-as_port=" + cameraConfig.getServerPort()
700 + "&-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");
705 public void setupSnapshotStreaming(boolean stream, ChannelHandlerContext ctx, boolean auto) {
707 sendMjpegFirstPacket(ctx);
709 autoSnapshotMjpegChannelGroup.add(ctx.channel());
710 lockCurrentSnapshot.lock();
712 sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
713 // iOS uses a FIFO? and needs two frames to display a pic
714 sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
716 lockCurrentSnapshot.unlock();
718 streamingAutoFps = true;
720 snapshotMjpegChannelGroup.add(ctx.channel());
721 lockCurrentSnapshot.lock();
723 sendMjpegFrame(currentSnapshot, snapshotMjpegChannelGroup);
725 lockCurrentSnapshot.unlock();
727 streamingSnapshotMjpeg = true;
728 startSnapshotPolling();
731 snapshotMjpegChannelGroup.remove(ctx.channel());
732 autoSnapshotMjpegChannelGroup.remove(ctx.channel());
733 if (streamingSnapshotMjpeg && snapshotMjpegChannelGroup.isEmpty()) {
734 streamingSnapshotMjpeg = false;
735 stopSnapshotPolling();
736 logger.debug("All snapshots.mjpeg streams have stopped.");
737 } else if (streamingAutoFps && autoSnapshotMjpegChannelGroup.isEmpty()) {
738 streamingAutoFps = false;
739 stopSnapshotPolling();
740 logger.debug("All autofps.mjpeg streams have stopped.");
745 // If start is true the CTX is added to the list to stream video to, false stops
747 public void setupMjpegStreaming(boolean start, ChannelHandlerContext ctx) {
749 if (mjpegChannelGroup.isEmpty()) {// first stream being requested.
750 mjpegChannelGroup.add(ctx.channel());
751 if (mjpegUri.isEmpty() || mjpegUri.equals("ffmpeg")) {
752 sendMjpegFirstPacket(ctx);
753 setupFfmpegFormat(FFmpegFormat.MJPEG);
756 // fix Dahua reboots when refreshing a mjpeg stream.
757 TimeUnit.MILLISECONDS.sleep(500);
758 } catch (InterruptedException e) {
760 sendHttpGET(mjpegUri);
762 } else if (ffmpegMjpeg != null) {// not first stream and we will use ffmpeg
763 sendMjpegFirstPacket(ctx);
764 mjpegChannelGroup.add(ctx.channel());
765 } else {// not first stream and camera supplies the mjpeg source.
766 ctx.channel().writeAndFlush(firstStreamedMsg);
767 mjpegChannelGroup.add(ctx.channel());
770 mjpegChannelGroup.remove(ctx.channel());
771 if (mjpegChannelGroup.isEmpty()) {
772 logger.debug("All ipcamera.mjpeg streams have stopped.");
773 if (mjpegUri.equals("ffmpeg") || mjpegUri.isEmpty()) {
774 Ffmpeg localMjpeg = ffmpegMjpeg;
775 if (localMjpeg != null) {
776 localMjpeg.stopConverting();
779 closeChannel(getTinyUrl(mjpegUri));
785 void openChannel(Channel channel, String httpRequestURL) {
786 ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
787 if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
788 tracker.setChannel(channel);
791 channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
794 void closeChannel(String url) {
795 ChannelTracking channelTracking = channelTrackingMap.get(url);
796 if (channelTracking != null) {
797 if (channelTracking.getChannel().isOpen()) {
798 channelTracking.getChannel().close();
805 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
806 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
809 void cleanChannels() {
810 for (Channel channel : openChannels) {
811 boolean oldChannel = true;
812 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
813 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
814 channelTrackingMap.remove(channelTracking.getRequestUrl());
816 if (channelTracking.getChannel() == channel) {
817 logger.trace("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
827 public void storeHttpReply(String url, String content) {
828 ChannelTracking channelTracking = channelTrackingMap.get(url);
829 if (channelTracking != null) {
830 channelTracking.setReply(content);
834 // sends direct to ctx so can be either snapshots.mjpeg or normal mjpeg stream
835 public void sendMjpegFirstPacket(ChannelHandlerContext ctx) {
836 final String boundary = "thisMjpegStream";
837 String contentType = "multipart/x-mixed-replace; boundary=" + boundary;
838 HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
839 response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
840 response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
841 response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
842 response.headers().add("Access-Control-Allow-Origin", "*");
843 response.headers().add("Access-Control-Expose-Headers", "*");
844 ctx.channel().writeAndFlush(response);
847 public void sendMjpegFrame(byte[] jpg, ChannelGroup channelGroup) {
848 final String boundary = "thisMjpegStream";
849 ByteBuf imageByteBuf = Unpooled.copiedBuffer(jpg);
850 int length = imageByteBuf.readableBytes();
851 String header = "--" + boundary + "\r\n" + "content-type: image/jpeg" + "\r\n" + "content-length: " + length
853 ByteBuf headerBbuf = Unpooled.copiedBuffer(header, 0, header.length(), StandardCharsets.UTF_8);
854 ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
855 streamToGroup(headerBbuf, channelGroup, false);
856 streamToGroup(imageByteBuf, channelGroup, false);
857 streamToGroup(footerBbuf, channelGroup, true);
860 public void streamToGroup(Object msg, ChannelGroup channelGroup, boolean flush) {
861 channelGroup.write(msg);
863 channelGroup.flush();
867 private void storeSnapshots() {
869 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
870 lockCurrentSnapshot.lock();
872 for (byte[] foo : fifoSnapshotBuffer) {
873 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
876 OutputStream fos = new FileOutputStream(file);
879 } catch (FileNotFoundException e) {
880 logger.warn("FileNotFoundException {}", e.getMessage());
881 } catch (IOException e) {
882 logger.warn("IOException {}", e.getMessage());
886 lockCurrentSnapshot.unlock();
890 public void setupFfmpegFormat(FFmpegFormat format) {
891 String inputOptions = cameraConfig.getFfmpegInputOptions();
892 if (cameraConfig.getFfmpegOutput().isEmpty()) {
893 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
896 if (rtspUri.isEmpty()) {
897 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
900 if (cameraConfig.getFfmpegLocation().isEmpty()) {
901 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
904 if (rtspUri.toLowerCase().contains("rtsp")) {
905 if (inputOptions.isEmpty()) {
906 inputOptions = "-rtsp_transport tcp";
910 // Make sure the folder exists, if not create it.
911 new File(cameraConfig.getFfmpegOutput()).mkdirs();
914 if (ffmpegHLS == null) {
915 if (!inputOptions.isEmpty()) {
916 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
917 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
918 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
919 cameraConfig.getUser(), cameraConfig.getPassword());
921 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
922 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
923 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
924 cameraConfig.getPassword());
927 Ffmpeg localHLS = ffmpegHLS;
928 if (localHLS != null) {
929 localHLS.startConverting();
933 if (cameraConfig.getGifPreroll() > 0) {
934 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
935 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
936 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
937 + cameraConfig.getGifOutOptions(),
938 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
939 cameraConfig.getPassword());
941 if (!inputOptions.isEmpty()) {
942 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
944 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
946 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
947 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
948 cameraConfig.getUser(), cameraConfig.getPassword());
950 if (cameraConfig.getGifPreroll() > 0) {
953 Ffmpeg localGIF = ffmpegGIF;
954 if (localGIF != null) {
955 localGIF.startConverting();
956 if (gifHistory.isEmpty()) {
957 gifHistory = gifFilename;
958 } else if (!gifFilename.equals("ipcamera")) {
959 gifHistory = gifFilename + "," + gifHistory;
960 if (gifHistoryLength > 49) {
961 int endIndex = gifHistory.lastIndexOf(",");
962 gifHistory = gifHistory.substring(0, endIndex);
965 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
969 if (!inputOptions.isEmpty()) {
970 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
972 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
974 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
975 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
976 cameraConfig.getUser(), cameraConfig.getPassword());
977 Ffmpeg localRecord = ffmpegRecord;
978 if (localRecord != null) {
979 localRecord.startConverting();
980 if (mp4History.isEmpty()) {
981 mp4History = mp4Filename;
982 } else if (!mp4Filename.equals("ipcamera")) {
983 mp4History = mp4Filename + "," + mp4History;
984 if (mp4HistoryLength > 49) {
985 int endIndex = mp4History.lastIndexOf(",");
986 mp4History = mp4History.substring(0, endIndex);
990 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
993 Ffmpeg localAlarms = ffmpegRtspHelper;
994 if (localAlarms != null) {
995 localAlarms.stopConverting();
996 if (!audioAlarmEnabled && !motionAlarmEnabled) {
1000 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
1001 String filterOptions = "";
1002 if (!audioAlarmEnabled) {
1003 filterOptions = "-an";
1005 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
1007 if (!motionAlarmEnabled && !ffmpegSnapshotGeneration) {
1008 filterOptions = filterOptions.concat(" -vn");
1009 } else if (motionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
1010 String usersMotionOptions = cameraConfig.getMotionOptions();
1011 if (usersMotionOptions.startsWith("-")) {
1012 // Need to put the users custom options first in the chain before the motion is detected
1013 filterOptions += " " + usersMotionOptions + ",select='gte(scene," + motionThreshold
1014 + ")',metadata=print";
1016 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
1017 + motionThreshold + ")',metadata=print";
1019 } else if (motionAlarmEnabled) {
1020 filterOptions = filterOptions
1021 .concat(" -vf select='gte(scene," + motionThreshold + ")',metadata=print");
1023 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
1024 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
1025 localAlarms = ffmpegRtspHelper;
1026 if (localAlarms != null) {
1027 localAlarms.startConverting();
1031 if (ffmpegMjpeg == null) {
1032 if (inputOptions.isEmpty()) {
1033 inputOptions = "-hide_banner -loglevel warning";
1035 inputOptions += " -hide_banner -loglevel warning";
1037 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1038 cameraConfig.getMjpegOptions(),
1039 "http://127.0.0.1:" + cameraConfig.getServerPort() + "/ipcamera.jpg",
1040 cameraConfig.getUser(), cameraConfig.getPassword());
1042 Ffmpeg localMjpeg = ffmpegMjpeg;
1043 if (localMjpeg != null) {
1044 localMjpeg.startConverting();
1048 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
1049 if (ffmpegSnapshot == null) {
1050 if (inputOptions.isEmpty()) {
1052 inputOptions = "-threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1054 inputOptions += " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1056 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1057 cameraConfig.getSnapshotOptions(),
1058 "http://127.0.0.1:" + cameraConfig.getServerPort() + "/snapshot.jpg",
1059 cameraConfig.getUser(), cameraConfig.getPassword());
1061 Ffmpeg localSnaps = ffmpegSnapshot;
1062 if (localSnaps != null) {
1063 localSnaps.startConverting();
1069 public void noMotionDetected(String thisAlarmsChannel) {
1070 setChannelState(thisAlarmsChannel, OnOffType.OFF);
1071 firstMotionAlarm = false;
1072 motionAlarmUpdateSnapshot = false;
1073 motionDetected = false;
1074 if (streamingAutoFps) {
1075 stopSnapshotPolling();
1076 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1077 stopSnapshotPolling();
1082 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
1083 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
1084 * tampering with the camera.
1086 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
1087 updateState(thisAlarmsChannel, state);
1090 public void motionDetected(String thisAlarmsChannel) {
1091 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
1092 updateState(thisAlarmsChannel, OnOffType.ON);
1093 motionDetected = true;
1094 if (streamingAutoFps) {
1095 startSnapshotPolling();
1097 if (cameraConfig.getUpdateImageWhen().contains("2")) {
1098 if (!firstMotionAlarm) {
1099 if (!snapshotUri.isEmpty()) {
1100 sendHttpGET(snapshotUri);
1102 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1104 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1105 if (!snapshotPolling) {
1106 startSnapshotPolling();
1108 firstMotionAlarm = true;
1109 motionAlarmUpdateSnapshot = true;
1113 public void audioDetected() {
1114 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1115 if (cameraConfig.getUpdateImageWhen().contains("3")) {
1116 if (!firstAudioAlarm) {
1117 if (!snapshotUri.isEmpty()) {
1118 sendHttpGET(snapshotUri);
1120 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1122 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1123 firstAudioAlarm = true;
1124 audioAlarmUpdateSnapshot = true;
1128 public void noAudioDetected() {
1129 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1130 firstAudioAlarm = false;
1131 audioAlarmUpdateSnapshot = false;
1134 public void recordMp4(String filename, int seconds) {
1135 mp4Filename = filename;
1136 mp4RecordTime = seconds;
1137 setupFfmpegFormat(FFmpegFormat.RECORD);
1138 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1141 public void recordGif(String filename, int seconds) {
1142 gifFilename = filename;
1143 gifRecordTime = seconds;
1144 if (cameraConfig.getGifPreroll() > 0) {
1145 snapCount = seconds;
1147 setupFfmpegFormat(FFmpegFormat.GIF);
1149 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1152 public String returnValueFromString(String rawString, String searchedString) {
1154 int index = rawString.indexOf(searchedString);
1155 if (index != -1) // -1 means "not found"
1157 result = rawString.substring(index + searchedString.length(), rawString.length());
1158 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1160 return result; // Did not find a carriage return.
1162 return result.substring(0, index);
1165 return ""; // Did not find the String we were searching for
1168 private void sendPTZRequest() {
1169 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1173 public void handleCommand(ChannelUID channelUID, Command command) {
1174 if (command instanceof RefreshType) {
1175 switch (channelUID.getId()) {
1177 if (onvifCamera.supportsPTZ()) {
1178 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1182 if (onvifCamera.supportsPTZ()) {
1183 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1187 if (onvifCamera.supportsPTZ()) {
1188 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1191 case CHANNEL_GOTO_PRESET:
1192 if (onvifCamera.supportsPTZ()) {
1193 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1197 } // caution "REFRESH" can still progress to brand Handlers below the else.
1199 switch (channelUID.getId()) {
1200 case CHANNEL_MP4_HISTORY_LENGTH:
1201 if (DecimalType.ZERO.equals(command)) {
1202 mp4HistoryLength = 0;
1204 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1207 case CHANNEL_GIF_HISTORY_LENGTH:
1208 if (DecimalType.ZERO.equals(command)) {
1209 gifHistoryLength = 0;
1211 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1214 case CHANNEL_FFMPEG_MOTION_CONTROL:
1215 if (OnOffType.ON.equals(command)) {
1216 motionAlarmEnabled = true;
1217 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1218 motionAlarmEnabled = false;
1219 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1221 motionAlarmEnabled = true;
1222 motionThreshold = Double.valueOf(command.toString());
1223 motionThreshold = motionThreshold / 10000;
1225 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1227 case CHANNEL_START_STREAM:
1229 if (OnOffType.ON.equals(command)) {
1230 localHLS = ffmpegHLS;
1231 if (localHLS == null) {
1232 setupFfmpegFormat(FFmpegFormat.HLS);
1233 localHLS = ffmpegHLS;
1235 if (localHLS != null) {
1236 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1237 localHLS.startConverting();
1240 localHLS = ffmpegHLS;
1241 if (localHLS != null) {
1242 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1243 localHLS.setKeepAlive(1);
1247 case CHANNEL_EXTERNAL_MOTION:
1248 if (OnOffType.ON.equals(command)) {
1249 motionDetected(CHANNEL_EXTERNAL_MOTION);
1251 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1254 case CHANNEL_GOTO_PRESET:
1255 if (onvifCamera.supportsPTZ()) {
1256 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1259 case CHANNEL_POLL_IMAGE:
1260 if (OnOffType.ON.equals(command)) {
1261 if (snapshotUri.isEmpty()) {
1262 ffmpegSnapshotGeneration = true;
1263 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1264 updateImageChannel = false;
1266 updateImageChannel = true;
1267 sendHttpGET(snapshotUri);// Allows this to change Image FPS on demand
1270 Ffmpeg localSnaps = ffmpegSnapshot;
1271 if (localSnaps != null) {
1272 localSnaps.stopConverting();
1273 ffmpegSnapshotGeneration = false;
1275 updateImageChannel = false;
1279 if (onvifCamera.supportsPTZ()) {
1280 if (command instanceof IncreaseDecreaseType) {
1281 if (command == IncreaseDecreaseType.INCREASE) {
1282 if (cameraConfig.getPtzContinuous()) {
1283 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1285 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1288 if (cameraConfig.getPtzContinuous()) {
1289 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1291 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1295 } else if (OnOffType.OFF.equals(command)) {
1296 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1299 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1300 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1304 if (onvifCamera.supportsPTZ()) {
1305 if (command instanceof IncreaseDecreaseType) {
1306 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1307 if (cameraConfig.getPtzContinuous()) {
1308 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1310 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1313 if (cameraConfig.getPtzContinuous()) {
1314 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1316 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1320 } else if (OnOffType.OFF.equals(command)) {
1321 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1324 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1325 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1329 if (onvifCamera.supportsPTZ()) {
1330 if (command instanceof IncreaseDecreaseType) {
1331 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1332 if (cameraConfig.getPtzContinuous()) {
1333 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1335 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1338 if (cameraConfig.getPtzContinuous()) {
1339 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1341 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1345 } else if (OnOffType.OFF.equals(command)) {
1346 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1349 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1350 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1355 // commands and refresh now get passed to brand handlers
1356 switch (thing.getThingTypeUID().getId()) {
1358 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1359 amcrestHandler.handleCommand(channelUID, command);
1360 if (lowPriorityRequests.isEmpty()) {
1361 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1365 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1366 dahuaHandler.handleCommand(channelUID, command);
1367 if (lowPriorityRequests.isEmpty()) {
1368 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1371 case DOORBIRD_THING:
1372 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1373 doorBirdHandler.handleCommand(channelUID, command);
1374 if (lowPriorityRequests.isEmpty()) {
1375 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1378 case HIKVISION_THING:
1379 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1380 hikvisionHandler.handleCommand(channelUID, command);
1381 if (lowPriorityRequests.isEmpty()) {
1382 lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1386 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1387 cameraConfig.getPassword());
1388 foscamHandler.handleCommand(channelUID, command);
1389 if (lowPriorityRequests.isEmpty()) {
1390 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1394 InstarHandler instarHandler = new InstarHandler(getHandle());
1395 instarHandler.handleCommand(channelUID, command);
1396 if (lowPriorityRequests.isEmpty()) {
1397 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1401 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1402 defaultHandler.handleCommand(channelUID, command);
1403 if (lowPriorityRequests.isEmpty()) {
1404 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1410 public void setChannelState(String channelToUpdate, State valueOf) {
1411 updateState(channelToUpdate, valueOf);
1414 void bringCameraOnline() {
1416 updateStatus(ThingStatus.ONLINE);
1417 groupTracker.listOfOnlineCameraHandlers.add(this);
1418 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1419 Future<?> localFuture = cameraConnectionJob;
1420 if (localFuture != null) {
1421 localFuture.cancel(false);
1424 switch (thing.getThingTypeUID().getId()) {
1425 case HIKVISION_THING:
1426 sendHttpGET("/ISAPI/Smart/AudioDetection/channels/" + cameraConfig.getNvrChannel() + "01");
1427 sendHttpGET("/ISAPI/Smart/LineDetection/" + cameraConfig.getNvrChannel() + "01");
1428 sendHttpGET("/ISAPI/Smart/FieldDetection/" + cameraConfig.getNvrChannel() + "01");
1430 "/ISAPI/System/Video/inputs/channels/" + cameraConfig.getNvrChannel() + "01/motionDetection");
1431 sendHttpGET("/ISAPI/System/Video/inputs/channels/" + cameraConfig.getNvrChannel() + "/overlays/text/1");
1432 sendHttpGET("/ISAPI/System/IO/inputs/" + cameraConfig.getNvrChannel());
1433 sendHttpGET("/ISAPI/System/IO/inputs/" + cameraConfig.getNvrChannel());
1437 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1438 snapshotPolling = true;
1439 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000, cameraConfig.getPollTime(),
1440 TimeUnit.MILLISECONDS);
1443 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1445 if (!rtspUri.isEmpty()) {
1446 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1448 if (updateImageChannel) {
1449 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1451 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1453 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1454 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1455 handle.cameraOnline(getThing().getUID().getId());
1460 void snapshotIsFfmpeg() {
1461 bringCameraOnline();
1462 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1464 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1465 if (!rtspUri.isEmpty()) {
1466 updateImageChannel = false;
1467 ffmpegSnapshotGeneration = true;
1468 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1469 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1471 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1475 void pollingCameraConnection() {
1476 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1477 if (rtspUri.isEmpty()) {
1478 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1480 if (snapshotUri.isEmpty() || snapshotUri.equals("ffmpeg")) {
1483 sendHttpRequest("GET", snapshotUri, null);
1487 if (!onvifCamera.isConnected()) {
1488 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1489 cameraConfig.getOnvifPort());
1490 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1492 if (snapshotUri.equals("ffmpeg")) {
1494 } else if (!snapshotUri.isEmpty()) {
1495 sendHttpRequest("GET", snapshotUri, null);
1496 } else if (!rtspUri.isEmpty()) {
1499 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1500 "Camera failed to report a valid Snaphot and/or RTSP URL. See readme on how to use the SNAPSHOT_URL_OVERRIDE feature.");
1504 public void cameraConfigError(String reason) {
1505 // wont try to reconnect again due to a config error being the cause.
1506 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1510 public void cameraCommunicationError(String reason) {
1511 // will try to reconnect again as camera may be rebooting.
1512 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1513 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1514 resetAndRetryConnecting();
1518 boolean streamIsStopped(String url) {
1519 ChannelTracking channelTracking = channelTrackingMap.get(url);
1520 if (channelTracking != null) {
1521 if (channelTracking.getChannel().isActive()) {
1522 return false; // stream is running.
1525 return true; // Stream stopped or never started.
1528 void snapshotRunnable() {
1529 // Snapshot should be first to keep consistent time between shots
1530 sendHttpGET(snapshotUri);
1531 if (snapCount > 0) {
1532 if (--snapCount == 0) {
1533 setupFfmpegFormat(FFmpegFormat.GIF);
1538 public void stopSnapshotPolling() {
1539 Future<?> localFuture;
1540 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1541 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1542 snapshotPolling = false;
1543 localFuture = snapshotJob;
1544 if (localFuture != null) {
1545 localFuture.cancel(true);
1547 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1548 snapshotPolling = false;
1549 localFuture = snapshotJob;
1550 if (localFuture != null) {
1551 localFuture.cancel(true);
1556 public void startSnapshotPolling() {
1557 if (snapshotPolling || ffmpegSnapshotGeneration) {
1558 return; // Already polling or creating with FFmpeg from RTSP
1560 if (streamingSnapshotMjpeg || streamingAutoFps) {
1561 snapshotPolling = true;
1562 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1563 TimeUnit.MILLISECONDS);
1564 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1565 snapshotPolling = true;
1566 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1567 TimeUnit.MILLISECONDS);
1572 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep mjpeg and alarm
1573 * streams open and more.
1576 void pollCameraRunnable() {
1577 // Snapshot should be first to keep consistent time between shots
1578 if (streamingAutoFps) {
1579 updateAutoFps = true;
1580 if (!snapshotPolling && !ffmpegSnapshotGeneration) {
1581 // Dont need to poll if creating from RTSP stream with FFmpeg or we are polling at full rate already.
1582 sendHttpGET(snapshotUri);
1584 } else if (!snapshotUri.isEmpty() && !snapshotPolling) {// we need to check camera is still online.
1585 sendHttpGET(snapshotUri);
1587 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1588 if (!lowPriorityRequests.isEmpty()) {
1589 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1590 lowPriorityCounter = 0;
1592 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1594 // what needs to be done every poll//
1595 switch (thing.getThingTypeUID().getId()) {
1599 if (!onvifCamera.isConnected()) {
1600 onvifCamera.connect(true);
1604 noMotionDetected(CHANNEL_MOTION_ALARM);
1605 noMotionDetected(CHANNEL_PIR_ALARM);
1608 case HIKVISION_THING:
1609 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1610 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1611 cameraConfig.getIp());
1612 sendHttpGET("/ISAPI/Event/notification/alertStream");
1616 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1617 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1620 // Check for alarms, channel for NVRs appears not to work at filtering.
1621 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1622 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1623 cameraConfig.getIp());
1624 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1627 case DOORBIRD_THING:
1628 // Check for alarms, channel for NVRs appears not to work at filtering.
1629 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1630 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1631 cameraConfig.getIp());
1632 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1636 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1637 + cameraConfig.getPassword());
1640 Ffmpeg localHLS = ffmpegHLS;
1641 if (localHLS != null) {
1642 localHLS.checkKeepAlive();
1644 if (openChannels.size() > 18) {
1645 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1651 public void initialize() {
1652 cameraConfig = getConfigAs(CameraConfig.class);
1653 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1654 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1655 rtspUri = cameraConfig.getFfmpegInput();
1657 if (cameraConfig.getServerPort() < 1) {
1659 "The Server Port is not set to a valid number which disables a lot of binding features. See readme for more info.");
1660 } else if (cameraConfig.getServerPort() < 1025) {
1661 logger.warn("The Server Port is <= 1024 and may cause permission errors under Linux, try a higher number.");
1664 // Known cameras will connect quicker if we skip ONVIF questions.
1665 switch (thing.getThingTypeUID().getId()) {
1668 if (mjpegUri.isEmpty()) {
1669 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1671 if (snapshotUri.isEmpty()) {
1672 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1675 case DOORBIRD_THING:
1676 if (mjpegUri.isEmpty()) {
1677 mjpegUri = "/bha-api/video.cgi";
1679 if (snapshotUri.isEmpty()) {
1680 snapshotUri = "/bha-api/image.cgi";
1684 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1685 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1686 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1687 if (mjpegUri.isEmpty()) {
1688 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1689 + cameraConfig.getPassword();
1691 if (snapshotUri.isEmpty()) {
1692 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1693 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1696 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1697 if (mjpegUri.isEmpty()) {
1698 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1700 if (snapshotUri.isEmpty()) {
1701 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1705 if (snapshotUri.isEmpty()) {
1706 snapshotUri = "/tmpfs/snap.jpg";
1708 if (mjpegUri.isEmpty()) {
1709 mjpegUri = "/mjpegstream.cgi?-chn=12";
1714 // Onvif and Instar event handling needs the host IP and the server started.
1715 if (cameraConfig.getServerPort() > 0) {
1716 startStreamServer();
1719 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1720 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1721 cameraConfig.getUser(), cameraConfig.getPassword());
1722 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1723 // Only use ONVIF events if it is not an API camera.
1724 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1727 // for poll times above 9 seconds don't display a warning about the Image channel.
1728 if (9000 <= cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1730 "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.");
1732 // Waiting 3 seconds for ONVIF to discover the urls before running.
1733 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 30, TimeUnit.SECONDS);
1736 // What the camera needs to re-connect if the initialize() is not called.
1737 private void resetAndRetryConnecting() {
1743 public void dispose() {
1745 snapshotPolling = false;
1746 onvifCamera.disconnect();
1747 Future<?> localFuture = pollCameraJob;
1748 if (localFuture != null) {
1749 localFuture.cancel(true);
1751 localFuture = snapshotJob;
1752 if (localFuture != null) {
1753 localFuture.cancel(true);
1755 localFuture = cameraConnectionJob;
1756 if (localFuture != null) {
1757 localFuture.cancel(true);
1759 threadPool.shutdown();
1760 threadPool = Executors.newScheduledThreadPool(4);
1762 groupTracker.listOfOnlineCameraHandlers.remove(this);
1763 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1764 // inform all group handlers that this camera has gone offline
1765 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1766 handle.cameraOffline(this);
1768 basicAuth = ""; // clear out stored Password hash
1769 useDigestAuth = false;
1771 openChannels.close();
1773 Ffmpeg localFfmpeg = ffmpegHLS;
1774 if (localFfmpeg != null) {
1775 localFfmpeg.stopConverting();
1778 localFfmpeg = ffmpegRecord;
1779 if (localFfmpeg != null) {
1780 localFfmpeg.stopConverting();
1782 localFfmpeg = ffmpegGIF;
1783 if (localFfmpeg != null) {
1784 localFfmpeg.stopConverting();
1786 localFfmpeg = ffmpegRtspHelper;
1787 if (localFfmpeg != null) {
1788 localFfmpeg.stopConverting();
1790 localFfmpeg = ffmpegMjpeg;
1791 if (localFfmpeg != null) {
1792 localFfmpeg.stopConverting();
1794 localFfmpeg = ffmpegSnapshot;
1795 if (localFfmpeg != null) {
1796 localFfmpeg.stopConverting();
1798 channelTrackingMap.clear();
1801 public void setStreamServerHandler(StreamServerHandler streamServerHandler2) {
1802 streamServerHandler = streamServerHandler2;
1805 public String getWhiteList() {
1806 return cameraConfig.getIpWhitelist();
1810 public Collection<Class<? extends ThingHandlerService>> getServices() {
1811 return Collections.singleton(IpCameraActions.class);