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,
594 channelTrackingMap.put(httpRequestURL, new ChannelTracking(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.");
698 public void setupSnapshotStreaming(boolean stream, ChannelHandlerContext ctx, boolean auto) {
700 sendMjpegFirstPacket(ctx);
702 autoSnapshotMjpegChannelGroup.add(ctx.channel());
703 lockCurrentSnapshot.lock();
705 sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
706 // iOS uses a FIFO? and needs two frames to display a pic
707 sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
709 lockCurrentSnapshot.unlock();
711 streamingAutoFps = true;
713 snapshotMjpegChannelGroup.add(ctx.channel());
714 lockCurrentSnapshot.lock();
716 sendMjpegFrame(currentSnapshot, snapshotMjpegChannelGroup);
718 lockCurrentSnapshot.unlock();
720 streamingSnapshotMjpeg = true;
721 startSnapshotPolling();
724 snapshotMjpegChannelGroup.remove(ctx.channel());
725 autoSnapshotMjpegChannelGroup.remove(ctx.channel());
726 if (streamingSnapshotMjpeg && snapshotMjpegChannelGroup.isEmpty()) {
727 streamingSnapshotMjpeg = false;
728 stopSnapshotPolling();
729 logger.debug("All snapshots.mjpeg streams have stopped.");
730 } else if (streamingAutoFps && autoSnapshotMjpegChannelGroup.isEmpty()) {
731 streamingAutoFps = false;
732 stopSnapshotPolling();
733 logger.debug("All autofps.mjpeg streams have stopped.");
738 // If start is true the CTX is added to the list to stream video to, false stops
740 public void setupMjpegStreaming(boolean start, ChannelHandlerContext ctx) {
742 if (mjpegChannelGroup.isEmpty()) {// first stream being requested.
743 mjpegChannelGroup.add(ctx.channel());
744 if (mjpegUri.isEmpty() || mjpegUri.equals("ffmpeg")) {
745 sendMjpegFirstPacket(ctx);
746 setupFfmpegFormat(FFmpegFormat.MJPEG);
749 // fix Dahua reboots when refreshing a mjpeg stream.
750 TimeUnit.MILLISECONDS.sleep(500);
751 } catch (InterruptedException e) {
753 sendHttpGET(mjpegUri);
755 } else if (ffmpegMjpeg != null) {// not first stream and we will use ffmpeg
756 sendMjpegFirstPacket(ctx);
757 mjpegChannelGroup.add(ctx.channel());
758 } else {// not first stream and camera supplies the mjpeg source.
759 ctx.channel().writeAndFlush(firstStreamedMsg);
760 mjpegChannelGroup.add(ctx.channel());
763 mjpegChannelGroup.remove(ctx.channel());
764 if (mjpegChannelGroup.isEmpty()) {
765 logger.debug("All ipcamera.mjpeg streams have stopped.");
766 if (mjpegUri.equals("ffmpeg") || mjpegUri.isEmpty()) {
767 Ffmpeg localMjpeg = ffmpegMjpeg;
768 if (localMjpeg != null) {
769 localMjpeg.stopConverting();
772 closeChannel(getTinyUrl(mjpegUri));
778 void closeChannel(String url) {
779 ChannelTracking channelTracking = channelTrackingMap.get(url);
780 if (channelTracking != null) {
781 if (channelTracking.getChannel().isOpen()) {
782 channelTracking.getChannel().close();
789 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
790 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
793 void cleanChannels() {
794 for (Channel channel : openChannels) {
795 boolean oldChannel = true;
796 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
797 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
798 channelTrackingMap.remove(channelTracking.getRequestUrl());
800 if (channelTracking.getChannel() == channel) {
801 logger.trace("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
811 public void storeHttpReply(String url, String content) {
812 ChannelTracking channelTracking = channelTrackingMap.get(url);
813 if (channelTracking != null) {
814 channelTracking.setReply(content);
818 // sends direct to ctx so can be either snapshots.mjpeg or normal mjpeg stream
819 public void sendMjpegFirstPacket(ChannelHandlerContext ctx) {
820 final String boundary = "thisMjpegStream";
821 String contentType = "multipart/x-mixed-replace; boundary=" + boundary;
822 HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
823 response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
824 response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
825 response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
826 response.headers().add("Access-Control-Allow-Origin", "*");
827 response.headers().add("Access-Control-Expose-Headers", "*");
828 ctx.channel().writeAndFlush(response);
831 public void sendMjpegFrame(byte[] jpg, ChannelGroup channelGroup) {
832 final String boundary = "thisMjpegStream";
833 ByteBuf imageByteBuf = Unpooled.copiedBuffer(jpg);
834 int length = imageByteBuf.readableBytes();
835 String header = "--" + boundary + "\r\n" + "content-type: image/jpeg" + "\r\n" + "content-length: " + length
837 ByteBuf headerBbuf = Unpooled.copiedBuffer(header, 0, header.length(), StandardCharsets.UTF_8);
838 ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
839 streamToGroup(headerBbuf, channelGroup, false);
840 streamToGroup(imageByteBuf, channelGroup, false);
841 streamToGroup(footerBbuf, channelGroup, true);
844 public void streamToGroup(Object msg, ChannelGroup channelGroup, boolean flush) {
845 channelGroup.write(msg);
847 channelGroup.flush();
851 private void storeSnapshots() {
853 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
854 lockCurrentSnapshot.lock();
856 for (byte[] foo : fifoSnapshotBuffer) {
857 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
860 OutputStream fos = new FileOutputStream(file);
863 } catch (FileNotFoundException e) {
864 logger.warn("FileNotFoundException {}", e.getMessage());
865 } catch (IOException e) {
866 logger.warn("IOException {}", e.getMessage());
870 lockCurrentSnapshot.unlock();
874 public void setupFfmpegFormat(FFmpegFormat format) {
875 String inputOptions = cameraConfig.getFfmpegInputOptions();
876 if (cameraConfig.getFfmpegOutput().isEmpty()) {
877 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
880 if (rtspUri.isEmpty()) {
881 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
884 if (cameraConfig.getFfmpegLocation().isEmpty()) {
885 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
888 if (rtspUri.toLowerCase().contains("rtsp")) {
889 if (inputOptions.isEmpty()) {
890 inputOptions = "-rtsp_transport tcp";
894 // Make sure the folder exists, if not create it.
895 new File(cameraConfig.getFfmpegOutput()).mkdirs();
898 if (ffmpegHLS == null) {
899 if (!inputOptions.isEmpty()) {
900 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
901 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
902 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
903 cameraConfig.getUser(), cameraConfig.getPassword());
905 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
906 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
907 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
908 cameraConfig.getPassword());
911 Ffmpeg localHLS = ffmpegHLS;
912 if (localHLS != null) {
913 localHLS.startConverting();
917 if (cameraConfig.getGifPreroll() > 0) {
918 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
919 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
920 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
921 + cameraConfig.getGifOutOptions(),
922 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
923 cameraConfig.getPassword());
925 if (!inputOptions.isEmpty()) {
926 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
928 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
930 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
931 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
932 cameraConfig.getUser(), cameraConfig.getPassword());
934 if (cameraConfig.getGifPreroll() > 0) {
937 Ffmpeg localGIF = ffmpegGIF;
938 if (localGIF != null) {
939 localGIF.startConverting();
940 if (gifHistory.isEmpty()) {
941 gifHistory = gifFilename;
942 } else if (!gifFilename.equals("ipcamera")) {
943 gifHistory = gifFilename + "," + gifHistory;
944 if (gifHistoryLength > 49) {
945 int endIndex = gifHistory.lastIndexOf(",");
946 gifHistory = gifHistory.substring(0, endIndex);
949 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
953 if (!inputOptions.isEmpty()) {
954 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
956 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
958 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
959 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
960 cameraConfig.getUser(), cameraConfig.getPassword());
961 Ffmpeg localRecord = ffmpegRecord;
962 if (localRecord != null) {
963 localRecord.startConverting();
964 if (mp4History.isEmpty()) {
965 mp4History = mp4Filename;
966 } else if (!mp4Filename.equals("ipcamera")) {
967 mp4History = mp4Filename + "," + mp4History;
968 if (mp4HistoryLength > 49) {
969 int endIndex = mp4History.lastIndexOf(",");
970 mp4History = mp4History.substring(0, endIndex);
974 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
977 Ffmpeg localAlarms = ffmpegRtspHelper;
978 if (localAlarms != null) {
979 localAlarms.stopConverting();
980 if (!audioAlarmEnabled && !motionAlarmEnabled) {
984 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
985 String filterOptions = "";
986 if (!audioAlarmEnabled) {
987 filterOptions = "-an";
989 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
991 if (!motionAlarmEnabled && !ffmpegSnapshotGeneration) {
992 filterOptions = filterOptions.concat(" -vn");
993 } else if (motionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
994 String usersMotionOptions = cameraConfig.getMotionOptions();
995 if (usersMotionOptions.startsWith("-")) {
996 // Need to put the users custom options first in the chain before the motion is detected
997 filterOptions += " " + usersMotionOptions + ",select='gte(scene," + motionThreshold
998 + ")',metadata=print";
1000 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
1001 + motionThreshold + ")',metadata=print";
1003 } else if (motionAlarmEnabled) {
1004 filterOptions = filterOptions
1005 .concat(" -vf select='gte(scene," + motionThreshold + ")',metadata=print");
1007 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
1008 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
1009 localAlarms = ffmpegRtspHelper;
1010 if (localAlarms != null) {
1011 localAlarms.startConverting();
1015 if (ffmpegMjpeg == null) {
1016 if (inputOptions.isEmpty()) {
1017 inputOptions = "-hide_banner -loglevel warning";
1019 inputOptions += " -hide_banner -loglevel warning";
1021 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1022 cameraConfig.getMjpegOptions(),
1023 "http://127.0.0.1:" + cameraConfig.getServerPort() + "/ipcamera.jpg",
1024 cameraConfig.getUser(), cameraConfig.getPassword());
1026 Ffmpeg localMjpeg = ffmpegMjpeg;
1027 if (localMjpeg != null) {
1028 localMjpeg.startConverting();
1032 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
1033 if (ffmpegSnapshot == null) {
1034 if (inputOptions.isEmpty()) {
1036 inputOptions = "-threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1038 inputOptions += " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1040 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1041 cameraConfig.getSnapshotOptions(),
1042 "http://127.0.0.1:" + cameraConfig.getServerPort() + "/snapshot.jpg",
1043 cameraConfig.getUser(), cameraConfig.getPassword());
1045 Ffmpeg localSnaps = ffmpegSnapshot;
1046 if (localSnaps != null) {
1047 localSnaps.startConverting();
1053 public void noMotionDetected(String thisAlarmsChannel) {
1054 setChannelState(thisAlarmsChannel, OnOffType.OFF);
1055 firstMotionAlarm = false;
1056 motionAlarmUpdateSnapshot = false;
1057 motionDetected = false;
1058 if (streamingAutoFps) {
1059 stopSnapshotPolling();
1060 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1061 stopSnapshotPolling();
1066 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
1067 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
1068 * tampering with the camera.
1070 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
1071 updateState(thisAlarmsChannel, state);
1074 public void motionDetected(String thisAlarmsChannel) {
1075 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
1076 updateState(thisAlarmsChannel, OnOffType.ON);
1077 motionDetected = true;
1078 if (streamingAutoFps) {
1079 startSnapshotPolling();
1081 if (cameraConfig.getUpdateImageWhen().contains("2")) {
1082 if (!firstMotionAlarm) {
1083 if (!snapshotUri.isEmpty()) {
1084 sendHttpGET(snapshotUri);
1086 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1088 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1089 if (!snapshotPolling) {
1090 startSnapshotPolling();
1092 firstMotionAlarm = true;
1093 motionAlarmUpdateSnapshot = true;
1097 public void audioDetected() {
1098 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1099 if (cameraConfig.getUpdateImageWhen().contains("3")) {
1100 if (!firstAudioAlarm) {
1101 if (!snapshotUri.isEmpty()) {
1102 sendHttpGET(snapshotUri);
1104 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1106 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1107 firstAudioAlarm = true;
1108 audioAlarmUpdateSnapshot = true;
1112 public void noAudioDetected() {
1113 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1114 firstAudioAlarm = false;
1115 audioAlarmUpdateSnapshot = false;
1118 public void recordMp4(String filename, int seconds) {
1119 mp4Filename = filename;
1120 mp4RecordTime = seconds;
1121 setupFfmpegFormat(FFmpegFormat.RECORD);
1122 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1125 public void recordGif(String filename, int seconds) {
1126 gifFilename = filename;
1127 gifRecordTime = seconds;
1128 if (cameraConfig.getGifPreroll() > 0) {
1129 snapCount = seconds;
1131 setupFfmpegFormat(FFmpegFormat.GIF);
1133 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1136 public String returnValueFromString(String rawString, String searchedString) {
1138 int index = rawString.indexOf(searchedString);
1139 if (index != -1) // -1 means "not found"
1141 result = rawString.substring(index + searchedString.length(), rawString.length());
1142 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1144 return result; // Did not find a carriage return.
1146 return result.substring(0, index);
1149 return ""; // Did not find the String we were searching for
1152 private void sendPTZRequest() {
1153 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1157 public void handleCommand(ChannelUID channelUID, Command command) {
1158 if (command instanceof RefreshType) {
1159 switch (channelUID.getId()) {
1161 if (onvifCamera.supportsPTZ()) {
1162 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1166 if (onvifCamera.supportsPTZ()) {
1167 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1171 if (onvifCamera.supportsPTZ()) {
1172 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1175 case CHANNEL_GOTO_PRESET:
1176 if (onvifCamera.supportsPTZ()) {
1177 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1181 } // caution "REFRESH" can still progress to brand Handlers below the else.
1183 switch (channelUID.getId()) {
1184 case CHANNEL_MP4_HISTORY_LENGTH:
1185 if (DecimalType.ZERO.equals(command)) {
1186 mp4HistoryLength = 0;
1188 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1191 case CHANNEL_GIF_HISTORY_LENGTH:
1192 if (DecimalType.ZERO.equals(command)) {
1193 gifHistoryLength = 0;
1195 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1198 case CHANNEL_FFMPEG_MOTION_CONTROL:
1199 if (OnOffType.ON.equals(command)) {
1200 motionAlarmEnabled = true;
1201 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1202 motionAlarmEnabled = false;
1203 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1205 motionAlarmEnabled = true;
1206 motionThreshold = Double.valueOf(command.toString());
1207 motionThreshold = motionThreshold / 10000;
1209 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1211 case CHANNEL_START_STREAM:
1213 if (OnOffType.ON.equals(command)) {
1214 localHLS = ffmpegHLS;
1215 if (localHLS == null) {
1216 setupFfmpegFormat(FFmpegFormat.HLS);
1217 localHLS = ffmpegHLS;
1219 if (localHLS != null) {
1220 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1221 localHLS.startConverting();
1224 localHLS = ffmpegHLS;
1225 if (localHLS != null) {
1226 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1227 localHLS.setKeepAlive(1);
1231 case CHANNEL_EXTERNAL_MOTION:
1232 if (OnOffType.ON.equals(command)) {
1233 motionDetected(CHANNEL_EXTERNAL_MOTION);
1235 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1238 case CHANNEL_GOTO_PRESET:
1239 if (onvifCamera.supportsPTZ()) {
1240 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1243 case CHANNEL_POLL_IMAGE:
1244 if (OnOffType.ON.equals(command)) {
1245 if (snapshotUri.isEmpty()) {
1246 ffmpegSnapshotGeneration = true;
1247 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1248 updateImageChannel = false;
1250 updateImageChannel = true;
1251 sendHttpGET(snapshotUri);// Allows this to change Image FPS on demand
1254 Ffmpeg localSnaps = ffmpegSnapshot;
1255 if (localSnaps != null) {
1256 localSnaps.stopConverting();
1257 ffmpegSnapshotGeneration = false;
1259 updateImageChannel = false;
1263 if (onvifCamera.supportsPTZ()) {
1264 if (command instanceof IncreaseDecreaseType) {
1265 if (command == IncreaseDecreaseType.INCREASE) {
1266 if (cameraConfig.getPtzContinuous()) {
1267 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1269 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1272 if (cameraConfig.getPtzContinuous()) {
1273 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1275 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1279 } else if (OnOffType.OFF.equals(command)) {
1280 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1283 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1284 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1288 if (onvifCamera.supportsPTZ()) {
1289 if (command instanceof IncreaseDecreaseType) {
1290 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1291 if (cameraConfig.getPtzContinuous()) {
1292 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1294 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1297 if (cameraConfig.getPtzContinuous()) {
1298 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1300 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1304 } else if (OnOffType.OFF.equals(command)) {
1305 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1308 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1309 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1313 if (onvifCamera.supportsPTZ()) {
1314 if (command instanceof IncreaseDecreaseType) {
1315 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1316 if (cameraConfig.getPtzContinuous()) {
1317 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1319 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1322 if (cameraConfig.getPtzContinuous()) {
1323 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1325 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1329 } else if (OnOffType.OFF.equals(command)) {
1330 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1333 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1334 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1339 // commands and refresh now get passed to brand handlers
1340 switch (thing.getThingTypeUID().getId()) {
1342 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1343 amcrestHandler.handleCommand(channelUID, command);
1344 if (lowPriorityRequests.isEmpty()) {
1345 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1349 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1350 dahuaHandler.handleCommand(channelUID, command);
1351 if (lowPriorityRequests.isEmpty()) {
1352 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1355 case DOORBIRD_THING:
1356 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1357 doorBirdHandler.handleCommand(channelUID, command);
1358 if (lowPriorityRequests.isEmpty()) {
1359 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1362 case HIKVISION_THING:
1363 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1364 hikvisionHandler.handleCommand(channelUID, command);
1365 if (lowPriorityRequests.isEmpty()) {
1366 lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1370 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1371 cameraConfig.getPassword());
1372 foscamHandler.handleCommand(channelUID, command);
1373 if (lowPriorityRequests.isEmpty()) {
1374 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1378 InstarHandler instarHandler = new InstarHandler(getHandle());
1379 instarHandler.handleCommand(channelUID, command);
1380 if (lowPriorityRequests.isEmpty()) {
1381 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1385 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1386 defaultHandler.handleCommand(channelUID, command);
1387 if (lowPriorityRequests.isEmpty()) {
1388 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1394 public void setChannelState(String channelToUpdate, State valueOf) {
1395 updateState(channelToUpdate, valueOf);
1398 void bringCameraOnline() {
1400 updateStatus(ThingStatus.ONLINE);
1401 groupTracker.listOfOnlineCameraHandlers.add(this);
1402 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1403 Future<?> localFuture = cameraConnectionJob;
1404 if (localFuture != null) {
1405 localFuture.cancel(false);
1408 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1409 snapshotPolling = true;
1410 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 1000, cameraConfig.getPollTime(),
1411 TimeUnit.MILLISECONDS);
1414 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1416 if (!rtspUri.isEmpty()) {
1417 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1419 if (updateImageChannel) {
1420 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1422 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1424 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1425 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1426 handle.cameraOnline(getThing().getUID().getId());
1431 void snapshotIsFfmpeg() {
1432 bringCameraOnline();
1433 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1435 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1436 if (!rtspUri.isEmpty()) {
1437 updateImageChannel = false;
1438 ffmpegSnapshotGeneration = true;
1439 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1440 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1442 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1446 void pollingCameraConnection() {
1447 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1448 if (rtspUri.isEmpty()) {
1449 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1451 if (snapshotUri.isEmpty() || snapshotUri.equals("ffmpeg")) {
1454 sendHttpRequest("GET", snapshotUri, null);
1458 if (!onvifCamera.isConnected()) {
1459 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1460 cameraConfig.getOnvifPort());
1461 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1463 if (snapshotUri.equals("ffmpeg")) {
1465 } else if (!snapshotUri.isEmpty()) {
1466 sendHttpRequest("GET", snapshotUri, null);
1467 } else if (!rtspUri.isEmpty()) {
1470 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1471 "Camera failed to report a valid Snaphot and/or RTSP URL. See readme on how to use the SNAPSHOT_URL_OVERRIDE feature.");
1475 public void cameraConfigError(String reason) {
1476 // wont try to reconnect again due to a config error being the cause.
1477 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1481 public void cameraCommunicationError(String reason) {
1482 // will try to reconnect again as camera may be rebooting.
1483 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1484 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1485 resetAndRetryConnecting();
1489 boolean streamIsStopped(String url) {
1490 ChannelTracking channelTracking = channelTrackingMap.get(url);
1491 if (channelTracking != null) {
1492 if (channelTracking.getChannel().isActive()) {
1493 return false; // stream is running.
1496 return true; // Stream stopped or never started.
1499 void snapshotRunnable() {
1500 // Snapshot should be first to keep consistent time between shots
1501 sendHttpGET(snapshotUri);
1502 if (snapCount > 0) {
1503 if (--snapCount == 0) {
1504 setupFfmpegFormat(FFmpegFormat.GIF);
1509 public void stopSnapshotPolling() {
1510 Future<?> localFuture;
1511 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1512 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1513 snapshotPolling = false;
1514 localFuture = snapshotJob;
1515 if (localFuture != null) {
1516 localFuture.cancel(true);
1518 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1519 snapshotPolling = false;
1520 localFuture = snapshotJob;
1521 if (localFuture != null) {
1522 localFuture.cancel(true);
1527 public void startSnapshotPolling() {
1528 if (snapshotPolling || ffmpegSnapshotGeneration) {
1529 return; // Already polling or creating with FFmpeg from RTSP
1531 if (streamingSnapshotMjpeg || streamingAutoFps) {
1532 snapshotPolling = true;
1533 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1534 TimeUnit.MILLISECONDS);
1535 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1536 snapshotPolling = true;
1537 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1538 TimeUnit.MILLISECONDS);
1543 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep mjpeg and alarm
1544 * streams open and more.
1547 void pollCameraRunnable() {
1548 // Snapshot should be first to keep consistent time between shots
1549 if (streamingAutoFps) {
1550 updateAutoFps = true;
1551 if (!snapshotPolling && !ffmpegSnapshotGeneration) {
1552 // Dont need to poll if creating from RTSP stream with FFmpeg or we are polling at full rate already.
1553 sendHttpGET(snapshotUri);
1555 } else if (!snapshotUri.isEmpty() && !snapshotPolling) {// we need to check camera is still online.
1556 sendHttpGET(snapshotUri);
1558 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1559 if (!lowPriorityRequests.isEmpty()) {
1560 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1561 lowPriorityCounter = 0;
1563 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1565 // what needs to be done every poll//
1566 switch (thing.getThingTypeUID().getId()) {
1570 if (!onvifCamera.isConnected()) {
1571 onvifCamera.connect(true);
1575 noMotionDetected(CHANNEL_MOTION_ALARM);
1576 noMotionDetected(CHANNEL_PIR_ALARM);
1579 case HIKVISION_THING:
1580 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1581 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1582 cameraConfig.getIp());
1583 sendHttpGET("/ISAPI/Event/notification/alertStream");
1587 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1588 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1591 // Check for alarms, channel for NVRs appears not to work at filtering.
1592 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1593 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1594 cameraConfig.getIp());
1595 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1598 case DOORBIRD_THING:
1599 // Check for alarms, channel for NVRs appears not to work at filtering.
1600 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1601 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1602 cameraConfig.getIp());
1603 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1607 Ffmpeg localHLS = ffmpegHLS;
1608 if (localHLS != null) {
1609 localHLS.checkKeepAlive();
1611 if (openChannels.size() > 18) {
1612 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1618 public void initialize() {
1619 cameraConfig = getConfigAs(CameraConfig.class);
1620 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1621 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1622 rtspUri = cameraConfig.getFfmpegInput();
1624 if (cameraConfig.getServerPort() < 1) {
1626 "The Server Port is not set to a valid number which disables a lot of binding features. See readme for more info.");
1627 } else if (cameraConfig.getServerPort() < 1025) {
1628 logger.warn("The Server Port is <= 1024 and may cause permission errors under Linux, try a higher number.");
1631 // Known cameras will connect quicker if we skip ONVIF questions.
1632 switch (thing.getThingTypeUID().getId()) {
1635 if (mjpegUri.isEmpty()) {
1636 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1638 if (snapshotUri.isEmpty()) {
1639 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1642 case DOORBIRD_THING:
1643 if (mjpegUri.isEmpty()) {
1644 mjpegUri = "/bha-api/video.cgi";
1646 if (snapshotUri.isEmpty()) {
1647 snapshotUri = "/bha-api/image.cgi";
1651 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1652 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1653 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1654 if (mjpegUri.isEmpty()) {
1655 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1656 + cameraConfig.getPassword();
1658 if (snapshotUri.isEmpty()) {
1659 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1660 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1663 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1664 if (mjpegUri.isEmpty()) {
1665 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1667 if (snapshotUri.isEmpty()) {
1668 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1672 if (snapshotUri.isEmpty()) {
1673 snapshotUri = "/tmpfs/snap.jpg";
1675 if (mjpegUri.isEmpty()) {
1676 mjpegUri = "/mjpegstream.cgi?-chn=12";
1681 // Onvif and Instar event handling needs the host IP and the server started.
1682 if (cameraConfig.getServerPort() > 0) {
1683 startStreamServer();
1686 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1687 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1688 cameraConfig.getUser(), cameraConfig.getPassword());
1689 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1690 // Only use ONVIF events if it is not an API camera.
1691 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1694 // for poll times above 9 seconds don't display a warning about the Image channel.
1695 if (9000 <= cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1697 "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.");
1699 // Waiting 3 seconds for ONVIF to discover the urls before running.
1700 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 30, TimeUnit.SECONDS);
1703 // What the camera needs to re-connect if the initialize() is not called.
1704 private void resetAndRetryConnecting() {
1710 public void dispose() {
1712 snapshotPolling = false;
1713 onvifCamera.disconnect();
1714 Future<?> localFuture = pollCameraJob;
1715 if (localFuture != null) {
1716 localFuture.cancel(true);
1718 localFuture = snapshotJob;
1719 if (localFuture != null) {
1720 localFuture.cancel(true);
1722 localFuture = cameraConnectionJob;
1723 if (localFuture != null) {
1724 localFuture.cancel(true);
1726 threadPool.shutdown();
1727 threadPool = Executors.newScheduledThreadPool(4);
1729 groupTracker.listOfOnlineCameraHandlers.remove(this);
1730 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1731 // inform all group handlers that this camera has gone offline
1732 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1733 handle.cameraOffline(this);
1735 basicAuth = ""; // clear out stored Password hash
1736 useDigestAuth = false;
1738 openChannels.close();
1740 Ffmpeg localFfmpeg = ffmpegHLS;
1741 if (localFfmpeg != null) {
1742 localFfmpeg.stopConverting();
1744 localFfmpeg = ffmpegRecord;
1745 if (localFfmpeg != null) {
1746 localFfmpeg.stopConverting();
1748 localFfmpeg = ffmpegGIF;
1749 if (localFfmpeg != null) {
1750 localFfmpeg.stopConverting();
1752 localFfmpeg = ffmpegRtspHelper;
1753 if (localFfmpeg != null) {
1754 localFfmpeg.stopConverting();
1756 localFfmpeg = ffmpegMjpeg;
1757 if (localFfmpeg != null) {
1758 localFfmpeg.stopConverting();
1760 localFfmpeg = ffmpegSnapshot;
1761 if (localFfmpeg != null) {
1762 localFfmpeg.stopConverting();
1764 channelTrackingMap.clear();
1767 public void setStreamServerHandler(StreamServerHandler streamServerHandler2) {
1768 streamServerHandler = streamServerHandler2;
1771 public String getWhiteList() {
1772 return cameraConfig.getIpWhitelist();
1776 public Collection<Class<? extends ThingHandlerService>> getServices() {
1777 return Collections.singleton(IpCameraActions.class);