2 * Copyright (c) 2010-2020 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
14 package org.openhab.binding.ipcamera.internal.handler;
16 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
19 import java.io.FileNotFoundException;
20 import java.io.FileOutputStream;
21 import java.io.IOException;
22 import java.io.OutputStream;
23 import java.net.InetSocketAddress;
24 import java.net.MalformedURLException;
26 import java.nio.charset.StandardCharsets;
27 import java.util.ArrayList;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.LinkedList;
31 import java.util.List;
33 import java.util.concurrent.ConcurrentHashMap;
34 import java.util.concurrent.Executors;
35 import java.util.concurrent.Future;
36 import java.util.concurrent.ScheduledExecutorService;
37 import java.util.concurrent.ScheduledFuture;
38 import java.util.concurrent.TimeUnit;
39 import java.util.concurrent.locks.ReentrantLock;
41 import org.eclipse.jdt.annotation.NonNullByDefault;
42 import org.eclipse.jdt.annotation.Nullable;
43 import org.openhab.binding.ipcamera.internal.AmcrestHandler;
44 import org.openhab.binding.ipcamera.internal.CameraConfig;
45 import org.openhab.binding.ipcamera.internal.ChannelTracking;
46 import org.openhab.binding.ipcamera.internal.DahuaHandler;
47 import org.openhab.binding.ipcamera.internal.DoorBirdHandler;
48 import org.openhab.binding.ipcamera.internal.Ffmpeg;
49 import org.openhab.binding.ipcamera.internal.FoscamHandler;
50 import org.openhab.binding.ipcamera.internal.GroupTracker;
51 import org.openhab.binding.ipcamera.internal.Helper;
52 import org.openhab.binding.ipcamera.internal.HikvisionHandler;
53 import org.openhab.binding.ipcamera.internal.HttpOnlyHandler;
54 import org.openhab.binding.ipcamera.internal.InstarHandler;
55 import org.openhab.binding.ipcamera.internal.IpCameraActions;
56 import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
57 import org.openhab.binding.ipcamera.internal.IpCameraDynamicStateDescriptionProvider;
58 import org.openhab.binding.ipcamera.internal.MyNettyAuthHandler;
59 import org.openhab.binding.ipcamera.internal.StreamServerHandler;
60 import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection;
61 import org.openhab.core.library.types.DecimalType;
62 import org.openhab.core.library.types.IncreaseDecreaseType;
63 import org.openhab.core.library.types.OnOffType;
64 import org.openhab.core.library.types.PercentType;
65 import org.openhab.core.library.types.RawType;
66 import org.openhab.core.library.types.StringType;
67 import org.openhab.core.thing.ChannelUID;
68 import org.openhab.core.thing.Thing;
69 import org.openhab.core.thing.ThingStatus;
70 import org.openhab.core.thing.ThingStatusDetail;
71 import org.openhab.core.thing.binding.BaseThingHandler;
72 import org.openhab.core.thing.binding.ThingHandlerService;
73 import org.openhab.core.types.Command;
74 import org.openhab.core.types.RefreshType;
75 import org.openhab.core.types.State;
76 import org.slf4j.Logger;
77 import org.slf4j.LoggerFactory;
79 import io.netty.bootstrap.Bootstrap;
80 import io.netty.bootstrap.ServerBootstrap;
81 import io.netty.buffer.ByteBuf;
82 import io.netty.buffer.Unpooled;
83 import io.netty.channel.Channel;
84 import io.netty.channel.ChannelDuplexHandler;
85 import io.netty.channel.ChannelFuture;
86 import io.netty.channel.ChannelFutureListener;
87 import io.netty.channel.ChannelHandlerContext;
88 import io.netty.channel.ChannelInitializer;
89 import io.netty.channel.ChannelOption;
90 import io.netty.channel.EventLoopGroup;
91 import io.netty.channel.group.ChannelGroup;
92 import io.netty.channel.group.DefaultChannelGroup;
93 import io.netty.channel.nio.NioEventLoopGroup;
94 import io.netty.channel.socket.SocketChannel;
95 import io.netty.channel.socket.nio.NioServerSocketChannel;
96 import io.netty.channel.socket.nio.NioSocketChannel;
97 import io.netty.handler.codec.base64.Base64;
98 import io.netty.handler.codec.http.DefaultFullHttpRequest;
99 import io.netty.handler.codec.http.DefaultHttpResponse;
100 import io.netty.handler.codec.http.FullHttpRequest;
101 import io.netty.handler.codec.http.HttpClientCodec;
102 import io.netty.handler.codec.http.HttpContent;
103 import io.netty.handler.codec.http.HttpHeaderNames;
104 import io.netty.handler.codec.http.HttpHeaderValues;
105 import io.netty.handler.codec.http.HttpMessage;
106 import io.netty.handler.codec.http.HttpMethod;
107 import io.netty.handler.codec.http.HttpResponse;
108 import io.netty.handler.codec.http.HttpResponseStatus;
109 import io.netty.handler.codec.http.HttpServerCodec;
110 import io.netty.handler.codec.http.HttpVersion;
111 import io.netty.handler.codec.http.LastHttpContent;
112 import io.netty.handler.stream.ChunkedWriteHandler;
113 import io.netty.handler.timeout.IdleState;
114 import io.netty.handler.timeout.IdleStateEvent;
115 import io.netty.handler.timeout.IdleStateHandler;
116 import io.netty.util.CharsetUtil;
117 import io.netty.util.ReferenceCountUtil;
118 import io.netty.util.concurrent.GlobalEventExecutor;
121 * The {@link IpCameraHandler} is responsible for handling commands, which are
122 * sent to one of the channels.
124 * @author Matthew Skinner - Initial contribution
128 public class IpCameraHandler extends BaseThingHandler {
129 public final Logger logger = LoggerFactory.getLogger(getClass());
130 public final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
131 private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(4);
132 private GroupTracker groupTracker;
133 public CameraConfig cameraConfig = new CameraConfig();
135 // ChannelGroup is thread safe
136 public final ChannelGroup mjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
137 private final ChannelGroup snapshotMjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
138 private final ChannelGroup autoSnapshotMjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
139 public final ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
140 public @Nullable Ffmpeg ffmpegHLS = null;
141 public @Nullable Ffmpeg ffmpegRecord = null;
142 public @Nullable Ffmpeg ffmpegGIF = null;
143 public @Nullable Ffmpeg ffmpegRtspHelper = null;
144 public @Nullable Ffmpeg ffmpegMjpeg = null;
145 public @Nullable Ffmpeg ffmpegSnapshot = null;
146 public boolean streamingAutoFps = false;
147 public boolean motionDetected = false;
149 private @Nullable ScheduledFuture<?> cameraConnectionJob = null;
150 private @Nullable ScheduledFuture<?> pollCameraJob = null;
151 private @Nullable ScheduledFuture<?> snapshotJob = null;
152 private @Nullable Bootstrap mainBootstrap;
153 private @Nullable ServerBootstrap serverBootstrap;
155 private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup();
156 private EventLoopGroup serversLoopGroup = new NioEventLoopGroup();
157 private FullHttpRequest putRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
159 private String gifFilename = "ipcamera";
160 private String gifHistory = "";
161 private String mp4History = "";
162 public int gifHistoryLength;
163 public int mp4HistoryLength;
164 private String mp4Filename = "ipcamera";
165 private int mp4RecordTime;
166 private int gifRecordTime = 5;
167 private LinkedList<byte[]> fifoSnapshotBuffer = new LinkedList<byte[]>();
168 private int snapCount;
169 private boolean updateImageChannel = false;
170 private boolean updateAutoFps = false;
171 private byte lowPriorityCounter = 0;
172 public String hostIp;
173 public Map<String, ChannelTracking> channelTrackingMap = new ConcurrentHashMap<>();
174 public List<String> lowPriorityRequests = new ArrayList<>(0);
176 // basicAuth MUST remain private as it holds the cameraConfig.getPassword()
177 private String basicAuth = "";
178 public boolean useBasicAuth = false;
179 public boolean useDigestAuth = false;
180 public String snapshotUri = "";
181 public String mjpegUri = "";
182 private @Nullable ChannelFuture serverFuture = null;
183 private Object firstStreamedMsg = new Object();
184 public byte[] currentSnapshot = new byte[] { (byte) 0x00 };
185 public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
186 public String rtspUri = "";
187 public boolean audioAlarmUpdateSnapshot = false;
188 private boolean motionAlarmUpdateSnapshot = false;
189 private boolean isOnline = false; // Used so only 1 error is logged when a network issue occurs.
190 private boolean firstAudioAlarm = false;
191 private boolean firstMotionAlarm = false;
192 public Double motionThreshold = 0.0016;
193 public int audioThreshold = 35;
194 @SuppressWarnings("unused")
195 private @Nullable StreamServerHandler streamServerHandler;
196 private boolean streamingSnapshotMjpeg = false;
197 public boolean motionAlarmEnabled = false;
198 public boolean audioAlarmEnabled = false;
199 public boolean ffmpegSnapshotGeneration = false;
200 public boolean snapshotPolling = false;
201 public OnvifConnection onvifCamera = new OnvifConnection(this, "", "", "");
203 // These methods handle the response from all camera brands, nothing specific to 1 brand.
204 private class CommonCameraHandler extends ChannelDuplexHandler {
205 private int bytesToRecieve = 0;
206 private int bytesAlreadyRecieved = 0;
207 private byte[] incomingJpeg = new byte[0];
208 private String incomingMessage = "";
209 private String contentType = "empty";
210 private Object reply = new Object();
211 private String requestUrl = "";
212 private boolean closeConnection = true;
213 private boolean isChunked = false;
215 public void setURL(String url) {
220 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
221 if (msg == null || ctx == null) {
225 if (msg instanceof HttpResponse) {
226 HttpResponse response = (HttpResponse) msg;
227 if (response.status().code() != 401) {
228 if (!response.headers().isEmpty()) {
229 for (String name : response.headers().names()) {
230 // Some cameras use first letter uppercase and others dont.
231 switch (name.toLowerCase()) { // Possible localization issues doing this
233 contentType = response.headers().getAsString(name);
235 case "content-length":
236 bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
239 if (response.headers().getAsString(name).contains("keep-alive")) {
240 closeConnection = false;
243 case "transfer-encoding":
244 if (response.headers().getAsString(name).contains("chunked")) {
250 if (contentType.contains("multipart")) {
251 closeConnection = false;
252 if (mjpegUri.contains(requestUrl)) {
253 if (msg instanceof HttpMessage) {
254 // very start of stream only
255 ReferenceCountUtil.retain(msg, 1);
256 firstStreamedMsg = msg;
257 streamToGroup(firstStreamedMsg, mjpegChannelGroup, true);
260 } else if (contentType.contains("image/jp")) {
261 if (bytesToRecieve == 0) {
262 bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
263 logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
265 incomingJpeg = new byte[bytesToRecieve];
270 if (msg instanceof HttpContent) {
271 if (mjpegUri.contains(requestUrl)) {
272 // multiple MJPEG stream packets come back as this.
273 ReferenceCountUtil.retain(msg, 1);
274 streamToGroup(msg, mjpegChannelGroup, true);
276 HttpContent content = (HttpContent) msg;
277 // Found some cameras uses Content-Type: image/jpg instead of image/jpeg
278 if (contentType.contains("image/jp")) {
279 for (int i = 0; i < content.content().capacity(); i++) {
280 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
282 if (content instanceof LastHttpContent) {
283 processSnapshot(incomingJpeg);
284 // testing next line and if works need to do a full cleanup of this function.
285 closeConnection = true;
286 if (closeConnection) {
290 bytesAlreadyRecieved = 0;
293 } else { // incomingMessage that is not an IMAGE
294 if (incomingMessage.isEmpty()) {
295 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
297 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
299 bytesAlreadyRecieved = incomingMessage.length();
300 if (content instanceof LastHttpContent) {
301 // If it is not an image send it on to the next handler//
302 if (bytesAlreadyRecieved != 0) {
303 reply = incomingMessage;
304 super.channelRead(ctx, reply);
307 // HIKVISION alertStream never has a LastHttpContent as it always stays open//
308 if (contentType.contains("multipart")) {
309 if (bytesAlreadyRecieved != 0) {
310 reply = incomingMessage;
311 incomingMessage = "";
313 bytesAlreadyRecieved = 0;
314 super.channelRead(ctx, reply);
317 // Foscam needs this as will other cameras with chunks//
318 if (isChunked && bytesAlreadyRecieved != 0) {
319 reply = incomingMessage;
320 super.channelRead(ctx, reply);
324 } else { // msg is not HttpContent
325 // Foscam and Amcrest 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 outputOptions = "-f null -";
986 String filterOptions = "";
987 if (!audioAlarmEnabled) {
988 filterOptions = "-an";
990 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
992 if (!motionAlarmEnabled && !ffmpegSnapshotGeneration) {
993 filterOptions = filterOptions.concat(" -vn");
994 } else if (motionAlarmEnabled) {
995 filterOptions = filterOptions
996 .concat(" -vf select='gte(scene," + motionThreshold + ")',metadata=print");
998 if (!cameraConfig.getUser().isEmpty()) {
999 filterOptions += " ";// add space as the Framework does not allow spaces at start of config.
1001 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
1002 filterOptions + cameraConfig.getMotionOptions(), outputOptions, cameraConfig.getUser(),
1003 cameraConfig.getPassword());
1004 localAlarms = ffmpegRtspHelper;
1005 if (localAlarms != null) {
1006 localAlarms.startConverting();
1010 if (ffmpegMjpeg == null) {
1011 if (inputOptions.isEmpty()) {
1012 inputOptions = "-hide_banner -loglevel warning";
1014 inputOptions += " -hide_banner -loglevel warning";
1016 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1017 cameraConfig.getMjpegOptions(),
1018 "http://127.0.0.1:" + cameraConfig.getServerPort() + "/ipcamera.jpg",
1019 cameraConfig.getUser(), cameraConfig.getPassword());
1021 Ffmpeg localMjpeg = ffmpegMjpeg;
1022 if (localMjpeg != null) {
1023 localMjpeg.startConverting();
1027 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
1028 if (ffmpegSnapshot == null) {
1029 if (inputOptions.isEmpty()) {
1031 inputOptions = "-threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1033 inputOptions += " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1035 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1036 cameraConfig.getSnapshotOptions(),
1037 "http://127.0.0.1:" + cameraConfig.getServerPort() + "/snapshot.jpg",
1038 cameraConfig.getUser(), cameraConfig.getPassword());
1040 Ffmpeg localSnaps = ffmpegSnapshot;
1041 if (localSnaps != null) {
1042 localSnaps.startConverting();
1048 public void noMotionDetected(String thisAlarmsChannel) {
1049 setChannelState(thisAlarmsChannel, OnOffType.OFF);
1050 firstMotionAlarm = false;
1051 motionAlarmUpdateSnapshot = false;
1052 motionDetected = false;
1053 if (streamingAutoFps) {
1054 stopSnapshotPolling();
1055 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1056 stopSnapshotPolling();
1061 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
1062 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
1063 * tampering with the camera.
1065 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
1066 updateState(thisAlarmsChannel, state);
1069 public void motionDetected(String thisAlarmsChannel) {
1070 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
1071 updateState(thisAlarmsChannel, OnOffType.ON);
1072 motionDetected = true;
1073 if (streamingAutoFps) {
1074 startSnapshotPolling();
1076 if (cameraConfig.getUpdateImageWhen().contains("2")) {
1077 if (!firstMotionAlarm) {
1078 if (!snapshotUri.isEmpty()) {
1079 sendHttpGET(snapshotUri);
1081 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1083 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1084 if (!snapshotPolling) {
1085 startSnapshotPolling();
1087 firstMotionAlarm = true;
1088 motionAlarmUpdateSnapshot = true;
1092 public void audioDetected() {
1093 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1094 if (cameraConfig.getUpdateImageWhen().contains("3")) {
1095 if (!firstAudioAlarm) {
1096 if (!snapshotUri.isEmpty()) {
1097 sendHttpGET(snapshotUri);
1099 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1101 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1102 firstAudioAlarm = true;
1103 audioAlarmUpdateSnapshot = true;
1107 public void noAudioDetected() {
1108 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1109 firstAudioAlarm = false;
1110 audioAlarmUpdateSnapshot = false;
1113 public void recordMp4(String filename, int seconds) {
1114 mp4Filename = filename;
1115 mp4RecordTime = seconds;
1116 setupFfmpegFormat(FFmpegFormat.RECORD);
1117 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1120 public void recordGif(String filename, int seconds) {
1121 gifFilename = filename;
1122 gifRecordTime = seconds;
1123 if (cameraConfig.getGifPreroll() > 0) {
1124 snapCount = seconds;
1126 setupFfmpegFormat(FFmpegFormat.GIF);
1128 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1131 public String returnValueFromString(String rawString, String searchedString) {
1133 int index = rawString.indexOf(searchedString);
1134 if (index != -1) // -1 means "not found"
1136 result = rawString.substring(index + searchedString.length(), rawString.length());
1137 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1139 return result; // Did not find a carriage return.
1141 return result.substring(0, index);
1144 return ""; // Did not find the String we were searching for
1147 private void sendPTZRequest() {
1148 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1152 public void handleCommand(ChannelUID channelUID, Command command) {
1153 if (command instanceof RefreshType) {
1154 switch (channelUID.getId()) {
1156 if (onvifCamera.supportsPTZ()) {
1157 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1161 if (onvifCamera.supportsPTZ()) {
1162 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1166 if (onvifCamera.supportsPTZ()) {
1167 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1170 case CHANNEL_GOTO_PRESET:
1171 if (onvifCamera.supportsPTZ()) {
1172 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1176 } // caution "REFRESH" can still progress to brand Handlers below the else.
1178 switch (channelUID.getId()) {
1179 case CHANNEL_MP4_HISTORY_LENGTH:
1180 if (DecimalType.ZERO.equals(command)) {
1181 mp4HistoryLength = 0;
1183 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1186 case CHANNEL_GIF_HISTORY_LENGTH:
1187 if (DecimalType.ZERO.equals(command)) {
1188 gifHistoryLength = 0;
1190 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1193 case CHANNEL_FFMPEG_MOTION_CONTROL:
1194 if (OnOffType.ON.equals(command)) {
1195 motionAlarmEnabled = true;
1196 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1197 motionAlarmEnabled = false;
1198 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1200 motionAlarmEnabled = true;
1201 motionThreshold = Double.valueOf(command.toString());
1202 motionThreshold = motionThreshold / 10000;
1204 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1206 case CHANNEL_START_STREAM:
1208 if (OnOffType.ON.equals(command)) {
1209 localHLS = ffmpegHLS;
1210 if (localHLS == null) {
1211 setupFfmpegFormat(FFmpegFormat.HLS);
1212 localHLS = ffmpegHLS;
1214 if (localHLS != null) {
1215 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1216 localHLS.startConverting();
1219 localHLS = ffmpegHLS;
1220 if (localHLS != null) {
1221 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1222 localHLS.setKeepAlive(1);
1226 case CHANNEL_EXTERNAL_MOTION:
1227 if (OnOffType.ON.equals(command)) {
1228 motionDetected(CHANNEL_EXTERNAL_MOTION);
1230 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1233 case CHANNEL_GOTO_PRESET:
1234 if (onvifCamera.supportsPTZ()) {
1235 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1238 case CHANNEL_POLL_IMAGE:
1239 if (OnOffType.ON.equals(command)) {
1240 if (snapshotUri.isEmpty()) {
1241 ffmpegSnapshotGeneration = true;
1242 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1243 updateImageChannel = false;
1245 updateImageChannel = true;
1246 sendHttpGET(snapshotUri);// Allows this to change Image FPS on demand
1249 Ffmpeg localSnaps = ffmpegSnapshot;
1250 if (localSnaps != null) {
1251 localSnaps.stopConverting();
1252 ffmpegSnapshotGeneration = false;
1254 updateImageChannel = false;
1258 if (onvifCamera.supportsPTZ()) {
1259 if (command instanceof IncreaseDecreaseType) {
1260 if (command == IncreaseDecreaseType.INCREASE) {
1261 if (cameraConfig.getPtzContinuous()) {
1262 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1264 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1267 if (cameraConfig.getPtzContinuous()) {
1268 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1270 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1274 } else if (OnOffType.OFF.equals(command)) {
1275 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1278 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1279 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1283 if (onvifCamera.supportsPTZ()) {
1284 if (command instanceof IncreaseDecreaseType) {
1285 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1286 if (cameraConfig.getPtzContinuous()) {
1287 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1289 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1292 if (cameraConfig.getPtzContinuous()) {
1293 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1295 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1299 } else if (OnOffType.OFF.equals(command)) {
1300 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1303 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1304 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1308 if (onvifCamera.supportsPTZ()) {
1309 if (command instanceof IncreaseDecreaseType) {
1310 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1311 if (cameraConfig.getPtzContinuous()) {
1312 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1314 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1317 if (cameraConfig.getPtzContinuous()) {
1318 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1320 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1324 } else if (OnOffType.OFF.equals(command)) {
1325 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1328 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1329 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1334 // commands and refresh now get passed to brand handlers
1335 switch (thing.getThingTypeUID().getId()) {
1337 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1338 amcrestHandler.handleCommand(channelUID, command);
1339 if (lowPriorityRequests.isEmpty()) {
1340 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1344 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1345 dahuaHandler.handleCommand(channelUID, command);
1346 if (lowPriorityRequests.isEmpty()) {
1347 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1350 case DOORBIRD_THING:
1351 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1352 doorBirdHandler.handleCommand(channelUID, command);
1353 if (lowPriorityRequests.isEmpty()) {
1354 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1357 case HIKVISION_THING:
1358 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1359 hikvisionHandler.handleCommand(channelUID, command);
1360 if (lowPriorityRequests.isEmpty()) {
1361 lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1365 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1366 cameraConfig.getPassword());
1367 foscamHandler.handleCommand(channelUID, command);
1368 if (lowPriorityRequests.isEmpty()) {
1369 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1373 InstarHandler instarHandler = new InstarHandler(getHandle());
1374 instarHandler.handleCommand(channelUID, command);
1375 if (lowPriorityRequests.isEmpty()) {
1376 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1380 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1381 defaultHandler.handleCommand(channelUID, command);
1382 if (lowPriorityRequests.isEmpty()) {
1383 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1389 public void setChannelState(String channelToUpdate, State valueOf) {
1390 updateState(channelToUpdate, valueOf);
1393 void bringCameraOnline() {
1395 updateStatus(ThingStatus.ONLINE);
1396 groupTracker.listOfOnlineCameraHandlers.add(this);
1397 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1398 Future<?> localFuture = cameraConnectionJob;
1399 if (localFuture != null) {
1400 localFuture.cancel(false);
1403 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1404 snapshotPolling = true;
1405 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 1000, cameraConfig.getPollTime(),
1406 TimeUnit.MILLISECONDS);
1409 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1411 if (!rtspUri.isEmpty()) {
1412 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1414 if (updateImageChannel) {
1415 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1417 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1419 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1420 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1421 handle.cameraOnline(getThing().getUID().getId());
1426 void snapshotIsFfmpeg() {
1427 bringCameraOnline();
1428 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1430 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1431 if (!rtspUri.isEmpty()) {
1432 updateImageChannel = false;
1433 ffmpegSnapshotGeneration = true;
1434 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1435 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1437 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1441 void pollingCameraConnection() {
1442 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1443 if (rtspUri.isEmpty()) {
1444 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1446 if (snapshotUri.isEmpty() || snapshotUri.equals("ffmpeg")) {
1449 sendHttpRequest("GET", snapshotUri, null);
1453 if (!onvifCamera.isConnected()) {
1454 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1455 cameraConfig.getOnvifPort());
1456 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1458 if (snapshotUri.equals("ffmpeg")) {
1460 } else if (!snapshotUri.isEmpty()) {
1461 sendHttpRequest("GET", snapshotUri, null);
1462 } else if (!rtspUri.isEmpty()) {
1465 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1466 "Camera failed to report a valid Snaphot and/or RTSP URL. See readme on how to use the SNAPSHOT_URL_OVERRIDE feature.");
1470 public void cameraConfigError(String reason) {
1471 // wont try to reconnect again due to a config error being the cause.
1472 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1476 public void cameraCommunicationError(String reason) {
1477 // will try to reconnect again as camera may be rebooting.
1478 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1479 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1480 resetAndRetryConnecting();
1484 boolean streamIsStopped(String url) {
1485 ChannelTracking channelTracking = channelTrackingMap.get(url);
1486 if (channelTracking != null) {
1487 if (channelTracking.getChannel().isOpen()) {
1488 return false; // stream is running.
1491 return true; // Stream stopped or never started.
1494 void snapshotRunnable() {
1495 // Snapshot should be first to keep consistent time between shots
1496 sendHttpGET(snapshotUri);
1497 if (snapCount > 0) {
1498 if (--snapCount == 0) {
1499 setupFfmpegFormat(FFmpegFormat.GIF);
1504 public void stopSnapshotPolling() {
1505 Future<?> localFuture;
1506 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1507 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1508 snapshotPolling = false;
1509 localFuture = snapshotJob;
1510 if (localFuture != null) {
1511 localFuture.cancel(true);
1513 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1514 snapshotPolling = false;
1515 localFuture = snapshotJob;
1516 if (localFuture != null) {
1517 localFuture.cancel(true);
1522 public void startSnapshotPolling() {
1523 if (snapshotPolling || ffmpegSnapshotGeneration) {
1524 return; // Already polling or creating with FFmpeg from RTSP
1526 if (streamingSnapshotMjpeg || streamingAutoFps) {
1527 snapshotPolling = true;
1528 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1529 TimeUnit.MILLISECONDS);
1530 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1531 snapshotPolling = true;
1532 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1533 TimeUnit.MILLISECONDS);
1537 // runs every 8 seconds due to mjpeg streams not staying open unless they update this often.
1538 void pollCameraRunnable() {
1539 // Snapshot should be first to keep consistent time between shots
1540 if (!snapshotUri.isEmpty()) {
1541 if (updateImageChannel) {
1542 sendHttpGET(snapshotUri);
1545 if (streamingAutoFps) {
1546 updateAutoFps = true;
1547 if (!snapshotPolling && !ffmpegSnapshotGeneration) {
1548 // Dont need to poll if creating from RTSP stream with FFmpeg or we are polling at full rate already.
1549 sendHttpGET(snapshotUri);
1552 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1553 if (!lowPriorityRequests.isEmpty()) {
1554 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1555 lowPriorityCounter = 0;
1557 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1559 // what needs to be done every poll//
1560 switch (thing.getThingTypeUID().getId()) {
1564 if (!onvifCamera.isConnected()) {
1565 onvifCamera.connect(true);
1569 noMotionDetected(CHANNEL_MOTION_ALARM);
1570 noMotionDetected(CHANNEL_PIR_ALARM);
1573 case HIKVISION_THING:
1574 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1575 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1576 cameraConfig.getIp());
1577 sendHttpGET("/ISAPI/Event/notification/alertStream");
1581 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1582 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1585 // Check for alarms, channel for NVRs appears not to work at filtering.
1586 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1587 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1588 cameraConfig.getIp());
1589 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1592 case DOORBIRD_THING:
1593 // Check for alarms, channel for NVRs appears not to work at filtering.
1594 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1595 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1596 cameraConfig.getIp());
1597 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1601 Ffmpeg localHLS = ffmpegHLS;
1602 if (localHLS != null) {
1603 localHLS.checkKeepAlive();
1605 if (openChannels.size() > 18) {
1606 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1612 public void initialize() {
1613 cameraConfig = getConfigAs(CameraConfig.class);
1614 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1615 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1616 rtspUri = cameraConfig.getFfmpegInput();
1618 if (cameraConfig.getServerPort() < 1) {
1620 "The Server Port is not set to a valid number which disables a lot of binding features. See readme for more info.");
1621 } else if (cameraConfig.getServerPort() < 1025) {
1622 logger.warn("The Server Port is <= 1024 and may cause permission errors under Linux, try a higher number.");
1625 // Known cameras will connect quicker if we skip ONVIF questions.
1626 switch (thing.getThingTypeUID().getId()) {
1629 if (mjpegUri.isEmpty()) {
1630 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1632 if (snapshotUri.isEmpty()) {
1633 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1636 case DOORBIRD_THING:
1637 if (mjpegUri.isEmpty()) {
1638 mjpegUri = "/bha-api/video.cgi";
1640 if (snapshotUri.isEmpty()) {
1641 snapshotUri = "/bha-api/image.cgi";
1645 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1646 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1647 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1648 if (mjpegUri.isEmpty()) {
1649 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1650 + cameraConfig.getPassword();
1652 if (snapshotUri.isEmpty()) {
1653 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1654 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1657 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1658 if (mjpegUri.isEmpty()) {
1659 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1661 if (snapshotUri.isEmpty()) {
1662 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1666 if (snapshotUri.isEmpty()) {
1667 snapshotUri = "/tmpfs/snap.jpg";
1669 if (mjpegUri.isEmpty()) {
1670 mjpegUri = "/mjpegstream.cgi?-chn=12";
1675 // Onvif and Instar event handling needs the host IP and the server started.
1676 if (cameraConfig.getServerPort() > 0) {
1677 startStreamServer();
1680 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1681 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1682 cameraConfig.getUser(), cameraConfig.getPassword());
1683 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1684 // Only use ONVIF events if it is not an API camera.
1685 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1688 // for poll times above 9 seconds don't display a warning about the Image channel.
1689 if (9000 <= cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1691 "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.");
1693 // Waiting 3 seconds for ONVIF to discover the urls before running.
1694 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 30, TimeUnit.SECONDS);
1697 // What the camera needs to re-connect if the initialize() is not called.
1698 private void resetAndRetryConnecting() {
1704 public void dispose() {
1706 snapshotPolling = false;
1707 onvifCamera.disconnect();
1708 Future<?> localFuture = pollCameraJob;
1709 if (localFuture != null) {
1710 localFuture.cancel(true);
1712 localFuture = snapshotJob;
1713 if (localFuture != null) {
1714 localFuture.cancel(true);
1716 localFuture = cameraConnectionJob;
1717 if (localFuture != null) {
1718 localFuture.cancel(true);
1720 threadPool.shutdown();
1721 threadPool = Executors.newScheduledThreadPool(4);
1723 groupTracker.listOfOnlineCameraHandlers.remove(this);
1724 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1725 // inform all group handlers that this camera has gone offline
1726 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1727 handle.cameraOffline(this);
1729 basicAuth = ""; // clear out stored Password hash
1730 useDigestAuth = false;
1732 openChannels.close();
1734 Ffmpeg localFfmpeg = ffmpegHLS;
1735 if (localFfmpeg != null) {
1736 localFfmpeg.stopConverting();
1738 localFfmpeg = ffmpegRecord;
1739 if (localFfmpeg != null) {
1740 localFfmpeg.stopConverting();
1742 localFfmpeg = ffmpegGIF;
1743 if (localFfmpeg != null) {
1744 localFfmpeg.stopConverting();
1746 localFfmpeg = ffmpegRtspHelper;
1747 if (localFfmpeg != null) {
1748 localFfmpeg.stopConverting();
1750 localFfmpeg = ffmpegMjpeg;
1751 if (localFfmpeg != null) {
1752 localFfmpeg.stopConverting();
1754 localFfmpeg = ffmpegSnapshot;
1755 if (localFfmpeg != null) {
1756 localFfmpeg.stopConverting();
1758 channelTrackingMap.clear();
1761 public void setStreamServerHandler(StreamServerHandler streamServerHandler2) {
1762 streamServerHandler = streamServerHandler2;
1765 public String getWhiteList() {
1766 return cameraConfig.getIpWhitelist();
1770 public Collection<Class<? extends ThingHandlerService>> getServices() {
1771 return Collections.singleton(IpCameraActions.class);