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.equals(requestUrl)) {
253 if (msg instanceof HttpMessage) {
254 // very start of stream only
255 ReferenceCountUtil.retain(msg, 1);
256 firstStreamedMsg = msg;
257 streamToGroup(firstStreamedMsg, mjpegChannelGroup, true);
260 } 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.equals(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 use 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 // Alarm Streams never have a LastHttpContent as they always stay open//
308 else 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 logger.debug("Reply is chunked.");
320 reply = incomingMessage;
321 super.channelRead(ctx, reply);
325 } else { // msg is not HttpContent
326 // Foscam cameras need this
327 if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
328 reply = incomingMessage;
329 logger.debug("Packet back from camera is {}", incomingMessage);
330 super.channelRead(ctx, reply);
334 ReferenceCountUtil.release(msg);
339 public void channelReadComplete(@Nullable ChannelHandlerContext ctx) {
343 public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
347 public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
351 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
352 if (cause == null || ctx == null) {
355 if (cause instanceof ArrayIndexOutOfBoundsException) {
356 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
359 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
366 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
370 if (evt instanceof IdleStateEvent) {
371 IdleStateEvent e = (IdleStateEvent) evt;
372 // If camera does not use the channel for X amount of time it will close.
373 if (e.state() == IdleState.READER_IDLE) {
374 String urlToKeepOpen = "";
375 switch (thing.getThingTypeUID().getId()) {
377 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
379 case HIKVISION_THING:
380 urlToKeepOpen = "/ISAPI/Event/notification/alertStream";
383 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
386 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
387 if (channelTracking != null) {
388 if (channelTracking.getChannel() == ctx.channel()) {
389 return; // don't auto close this as it is for the alarms.
398 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
399 IpCameraDynamicStateDescriptionProvider stateDescriptionProvider) {
401 this.stateDescriptionProvider = stateDescriptionProvider;
402 if (ipAddress != null) {
405 hostIp = Helper.getLocalIpAddress();
407 this.groupTracker = groupTracker;
410 private IpCameraHandler getHandle() {
414 // false clears the stored user/pass hash, true creates the hash
415 public boolean setBasicAuth(boolean useBasic) {
417 logger.debug("Clearing out the stored BASIC auth now.");
420 } else if (!basicAuth.isEmpty()) {
421 // due to camera may have been sent multiple requests before the auth was set, this may trigger falsely.
422 logger.warn("Camera is reporting your username and/or password is wrong.");
425 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
426 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
427 ByteBuf byteBuf = null;
429 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
430 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
432 if (byteBuf != null) {
438 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
443 private String getCorrectUrlFormat(String longUrl) {
444 String temp = longUrl;
447 if (longUrl.isEmpty() || longUrl.equals("ffmpeg")) {
452 url = new URL(longUrl);
453 int port = url.getPort();
455 if (url.getQuery() == null) {
456 temp = url.getPath();
458 temp = url.getPath() + "?" + url.getQuery();
461 if (url.getQuery() == null) {
462 temp = ":" + url.getPort() + url.getPath();
464 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
467 } catch (MalformedURLException e) {
468 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
473 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
474 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
475 sendHttpRequest("PUT", httpRequestURL, null);
478 public void sendHttpGET(String httpRequestURL) {
479 sendHttpRequest("GET", httpRequestURL, null);
482 public int getPortFromShortenedUrl(String httpRequestURL) {
483 if (httpRequestURL.startsWith(":")) {
484 int end = httpRequestURL.indexOf("/");
485 return Integer.parseInt(httpRequestURL.substring(1, end));
487 return cameraConfig.getPort();
490 public String getTinyUrl(String httpRequestURL) {
491 if (httpRequestURL.startsWith(":")) {
492 int beginIndex = httpRequestURL.indexOf("/");
493 return httpRequestURL.substring(beginIndex);
495 return httpRequestURL;
498 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
499 // The authHandler will generate a digest string and re-send using this same function when needed.
500 @SuppressWarnings("null")
501 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
502 int port = getPortFromShortenedUrl(httpRequestURLFull);
503 String httpRequestURL = getTinyUrl(httpRequestURLFull);
505 if (mainBootstrap == null) {
506 mainBootstrap = new Bootstrap();
507 mainBootstrap.group(mainEventLoopGroup);
508 mainBootstrap.channel(NioSocketChannel.class);
509 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
510 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
511 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
512 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
513 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
514 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
517 public void initChannel(SocketChannel socketChannel) throws Exception {
518 // HIK Alarm stream needs > 9sec idle to stop stream closing
519 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
520 socketChannel.pipeline().addLast(new HttpClientCodec());
521 socketChannel.pipeline().addLast(AUTH_HANDLER,
522 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
523 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
525 switch (thing.getThingTypeUID().getId()) {
527 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
530 socketChannel.pipeline()
531 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
534 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
537 socketChannel.pipeline().addLast(
538 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
540 case HIKVISION_THING:
541 socketChannel.pipeline()
542 .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
545 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
548 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
555 FullHttpRequest request;
556 if (!"PUT".equals(httpMethod) || (useDigestAuth && digestString == null)) {
557 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
558 request.headers().set("Host", cameraConfig.getIp() + ":" + port);
559 request.headers().set("Connection", HttpHeaderValues.KEEP_ALIVE);
561 request = putRequestWithBody;
564 if (!basicAuth.isEmpty()) {
566 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
569 request.headers().set("Authorization", "Basic " + basicAuth);
574 if (digestString != null) {
575 request.headers().set("Authorization", "Digest " + digestString);
579 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
580 .addListener(new ChannelFutureListener() {
583 public void operationComplete(@Nullable ChannelFuture future) {
584 if (future == null) {
587 if (future.isDone() && future.isSuccess()) {
588 Channel ch = future.channel();
589 openChannels.add(ch);
593 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
595 channelTrackingMap.put(httpRequestURL, new ChannelTracking(ch, httpRequestURL));
597 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
598 commonHandler.setURL(httpRequestURLFull);
599 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
600 authHandler.setURL(httpMethod, httpRequestURL);
602 switch (thing.getThingTypeUID().getId()) {
604 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
605 amcrestHandler.setURL(httpRequestURL);
608 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
609 instarHandler.setURL(httpRequestURL);
612 ch.writeAndFlush(request);
613 } else { // an error occured
614 cameraCommunicationError(
615 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
621 public void processSnapshot(byte[] incommingSnapshot) {
622 lockCurrentSnapshot.lock();
624 currentSnapshot = incommingSnapshot;
625 if (cameraConfig.getGifPreroll() > 0) {
626 fifoSnapshotBuffer.add(incommingSnapshot);
627 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
628 fifoSnapshotBuffer.removeFirst();
632 lockCurrentSnapshot.unlock();
635 if (streamingSnapshotMjpeg) {
636 sendMjpegFrame(incommingSnapshot, snapshotMjpegChannelGroup);
638 if (streamingAutoFps) {
639 if (motionDetected) {
640 sendMjpegFrame(incommingSnapshot, autoSnapshotMjpegChannelGroup);
641 } else if (updateAutoFps) {
642 // only happens every 8 seconds as some browsers need a frame that often to keep stream alive.
643 sendMjpegFrame(incommingSnapshot, autoSnapshotMjpegChannelGroup);
644 updateAutoFps = false;
648 if (updateImageChannel) {
649 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
650 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
651 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
652 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
653 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
654 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
655 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
659 public void stopStreamServer() {
660 serversLoopGroup.shutdownGracefully();
661 serverBootstrap = null;
664 @SuppressWarnings("null")
665 public void startStreamServer() {
666 if (serverBootstrap == null) {
668 serversLoopGroup = new NioEventLoopGroup();
669 serverBootstrap = new ServerBootstrap();
670 serverBootstrap.group(serversLoopGroup);
671 serverBootstrap.channel(NioServerSocketChannel.class);
672 // IP "0.0.0.0" will bind the server to all network connections//
673 serverBootstrap.localAddress(new InetSocketAddress("0.0.0.0", cameraConfig.getServerPort()));
674 serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
676 protected void initChannel(SocketChannel socketChannel) throws Exception {
677 socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 60, 0));
678 socketChannel.pipeline().addLast("HttpServerCodec", new HttpServerCodec());
679 socketChannel.pipeline().addLast("ChunkedWriteHandler", new ChunkedWriteHandler());
680 socketChannel.pipeline().addLast("streamServerHandler", new StreamServerHandler(getHandle()));
683 serverFuture = serverBootstrap.bind().sync();
684 serverFuture.await(4000);
685 logger.debug("File server for camera at {} has started on port {} for all NIC's.", cameraConfig.getIp(),
686 cameraConfig.getServerPort());
687 updateState(CHANNEL_MJPEG_URL,
688 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.mjpeg"));
689 updateState(CHANNEL_HLS_URL,
690 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.m3u8"));
691 updateState(CHANNEL_IMAGE_URL,
692 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.jpg"));
693 } catch (Exception e) {
694 cameraConfigError("Exception when starting server. Try changing the Server Port to another number.");
699 public void setupSnapshotStreaming(boolean stream, ChannelHandlerContext ctx, boolean auto) {
701 sendMjpegFirstPacket(ctx);
703 autoSnapshotMjpegChannelGroup.add(ctx.channel());
704 lockCurrentSnapshot.lock();
706 sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
707 // iOS uses a FIFO? and needs two frames to display a pic
708 sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
710 lockCurrentSnapshot.unlock();
712 streamingAutoFps = true;
714 snapshotMjpegChannelGroup.add(ctx.channel());
715 lockCurrentSnapshot.lock();
717 sendMjpegFrame(currentSnapshot, snapshotMjpegChannelGroup);
719 lockCurrentSnapshot.unlock();
721 streamingSnapshotMjpeg = true;
722 startSnapshotPolling();
725 snapshotMjpegChannelGroup.remove(ctx.channel());
726 autoSnapshotMjpegChannelGroup.remove(ctx.channel());
727 if (streamingSnapshotMjpeg && snapshotMjpegChannelGroup.isEmpty()) {
728 streamingSnapshotMjpeg = false;
729 stopSnapshotPolling();
730 logger.debug("All snapshots.mjpeg streams have stopped.");
731 } else if (streamingAutoFps && autoSnapshotMjpegChannelGroup.isEmpty()) {
732 streamingAutoFps = false;
733 stopSnapshotPolling();
734 logger.debug("All autofps.mjpeg streams have stopped.");
739 // If start is true the CTX is added to the list to stream video to, false stops
741 public void setupMjpegStreaming(boolean start, ChannelHandlerContext ctx) {
743 if (mjpegChannelGroup.isEmpty()) {// first stream being requested.
744 mjpegChannelGroup.add(ctx.channel());
745 if (mjpegUri.isEmpty() || mjpegUri.equals("ffmpeg")) {
746 sendMjpegFirstPacket(ctx);
747 setupFfmpegFormat(FFmpegFormat.MJPEG);
750 // fix Dahua reboots when refreshing a mjpeg stream.
751 TimeUnit.MILLISECONDS.sleep(500);
752 } catch (InterruptedException e) {
754 sendHttpGET(mjpegUri);
756 } else if (ffmpegMjpeg != null) {// not first stream and we will use ffmpeg
757 sendMjpegFirstPacket(ctx);
758 mjpegChannelGroup.add(ctx.channel());
759 } else {// not first stream and camera supplies the mjpeg source.
760 ctx.channel().writeAndFlush(firstStreamedMsg);
761 mjpegChannelGroup.add(ctx.channel());
764 mjpegChannelGroup.remove(ctx.channel());
765 if (mjpegChannelGroup.isEmpty()) {
766 logger.debug("All ipcamera.mjpeg streams have stopped.");
767 if (mjpegUri.equals("ffmpeg") || mjpegUri.isEmpty()) {
768 Ffmpeg localMjpeg = ffmpegMjpeg;
769 if (localMjpeg != null) {
770 localMjpeg.stopConverting();
773 closeChannel(getTinyUrl(mjpegUri));
779 void closeChannel(String url) {
780 ChannelTracking channelTracking = channelTrackingMap.get(url);
781 if (channelTracking != null) {
782 if (channelTracking.getChannel().isOpen()) {
783 channelTracking.getChannel().close();
790 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
791 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
794 void cleanChannels() {
795 for (Channel channel : openChannels) {
796 boolean oldChannel = true;
797 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
798 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
799 channelTrackingMap.remove(channelTracking.getRequestUrl());
801 if (channelTracking.getChannel() == channel) {
802 logger.trace("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
812 public void storeHttpReply(String url, String content) {
813 ChannelTracking channelTracking = channelTrackingMap.get(url);
814 if (channelTracking != null) {
815 channelTracking.setReply(content);
819 // sends direct to ctx so can be either snapshots.mjpeg or normal mjpeg stream
820 public void sendMjpegFirstPacket(ChannelHandlerContext ctx) {
821 final String boundary = "thisMjpegStream";
822 String contentType = "multipart/x-mixed-replace; boundary=" + boundary;
823 HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
824 response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
825 response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
826 response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
827 response.headers().add("Access-Control-Allow-Origin", "*");
828 response.headers().add("Access-Control-Expose-Headers", "*");
829 ctx.channel().writeAndFlush(response);
832 public void sendMjpegFrame(byte[] jpg, ChannelGroup channelGroup) {
833 final String boundary = "thisMjpegStream";
834 ByteBuf imageByteBuf = Unpooled.copiedBuffer(jpg);
835 int length = imageByteBuf.readableBytes();
836 String header = "--" + boundary + "\r\n" + "content-type: image/jpeg" + "\r\n" + "content-length: " + length
838 ByteBuf headerBbuf = Unpooled.copiedBuffer(header, 0, header.length(), StandardCharsets.UTF_8);
839 ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
840 streamToGroup(headerBbuf, channelGroup, false);
841 streamToGroup(imageByteBuf, channelGroup, false);
842 streamToGroup(footerBbuf, channelGroup, true);
845 public void streamToGroup(Object msg, ChannelGroup channelGroup, boolean flush) {
846 channelGroup.write(msg);
848 channelGroup.flush();
852 private void storeSnapshots() {
854 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
855 lockCurrentSnapshot.lock();
857 for (byte[] foo : fifoSnapshotBuffer) {
858 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
861 OutputStream fos = new FileOutputStream(file);
864 } catch (FileNotFoundException e) {
865 logger.warn("FileNotFoundException {}", e.getMessage());
866 } catch (IOException e) {
867 logger.warn("IOException {}", e.getMessage());
871 lockCurrentSnapshot.unlock();
875 public void setupFfmpegFormat(FFmpegFormat format) {
876 String inputOptions = cameraConfig.getFfmpegInputOptions();
877 if (cameraConfig.getFfmpegOutput().isEmpty()) {
878 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
881 if (rtspUri.isEmpty()) {
882 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
885 if (cameraConfig.getFfmpegLocation().isEmpty()) {
886 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
889 if (rtspUri.toLowerCase().contains("rtsp")) {
890 if (inputOptions.isEmpty()) {
891 inputOptions = "-rtsp_transport tcp";
895 // Make sure the folder exists, if not create it.
896 new File(cameraConfig.getFfmpegOutput()).mkdirs();
899 if (ffmpegHLS == null) {
900 if (!inputOptions.isEmpty()) {
901 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
902 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
903 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
904 cameraConfig.getUser(), cameraConfig.getPassword());
906 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
907 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
908 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
909 cameraConfig.getPassword());
912 Ffmpeg localHLS = ffmpegHLS;
913 if (localHLS != null) {
914 localHLS.startConverting();
918 if (cameraConfig.getGifPreroll() > 0) {
919 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
920 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
921 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
922 + cameraConfig.getGifOutOptions(),
923 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
924 cameraConfig.getPassword());
926 if (!inputOptions.isEmpty()) {
927 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
929 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
931 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
932 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
933 cameraConfig.getUser(), cameraConfig.getPassword());
935 if (cameraConfig.getGifPreroll() > 0) {
938 Ffmpeg localGIF = ffmpegGIF;
939 if (localGIF != null) {
940 localGIF.startConverting();
941 if (gifHistory.isEmpty()) {
942 gifHistory = gifFilename;
943 } else if (!gifFilename.equals("ipcamera")) {
944 gifHistory = gifFilename + "," + gifHistory;
945 if (gifHistoryLength > 49) {
946 int endIndex = gifHistory.lastIndexOf(",");
947 gifHistory = gifHistory.substring(0, endIndex);
950 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
954 if (!inputOptions.isEmpty()) {
955 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
957 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
959 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
960 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
961 cameraConfig.getUser(), cameraConfig.getPassword());
962 Ffmpeg localRecord = ffmpegRecord;
963 if (localRecord != null) {
964 localRecord.startConverting();
965 if (mp4History.isEmpty()) {
966 mp4History = mp4Filename;
967 } else if (!mp4Filename.equals("ipcamera")) {
968 mp4History = mp4Filename + "," + mp4History;
969 if (mp4HistoryLength > 49) {
970 int endIndex = mp4History.lastIndexOf(",");
971 mp4History = mp4History.substring(0, endIndex);
975 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
978 Ffmpeg localAlarms = ffmpegRtspHelper;
979 if (localAlarms != null) {
980 localAlarms.stopConverting();
981 if (!audioAlarmEnabled && !motionAlarmEnabled) {
985 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
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 && !cameraConfig.getMotionOptions().isEmpty()) {
995 String usersMotionOptions = cameraConfig.getMotionOptions();
996 if (usersMotionOptions.startsWith("-")) {
997 // Need to put the users custom options first in the chain before the motion is detected
998 filterOptions += " " + usersMotionOptions + ",select='gte(scene," + motionThreshold
999 + ")',metadata=print";
1001 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
1002 + motionThreshold + ")',metadata=print";
1004 } else if (motionAlarmEnabled) {
1005 filterOptions = filterOptions
1006 .concat(" -vf select='gte(scene," + motionThreshold + ")',metadata=print");
1008 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
1009 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
1010 localAlarms = ffmpegRtspHelper;
1011 if (localAlarms != null) {
1012 localAlarms.startConverting();
1016 if (ffmpegMjpeg == null) {
1017 if (inputOptions.isEmpty()) {
1018 inputOptions = "-hide_banner -loglevel warning";
1020 inputOptions += " -hide_banner -loglevel warning";
1022 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1023 cameraConfig.getMjpegOptions(),
1024 "http://127.0.0.1:" + cameraConfig.getServerPort() + "/ipcamera.jpg",
1025 cameraConfig.getUser(), cameraConfig.getPassword());
1027 Ffmpeg localMjpeg = ffmpegMjpeg;
1028 if (localMjpeg != null) {
1029 localMjpeg.startConverting();
1033 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
1034 if (ffmpegSnapshot == null) {
1035 if (inputOptions.isEmpty()) {
1037 inputOptions = "-threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1039 inputOptions += " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1041 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1042 cameraConfig.getSnapshotOptions(),
1043 "http://127.0.0.1:" + cameraConfig.getServerPort() + "/snapshot.jpg",
1044 cameraConfig.getUser(), cameraConfig.getPassword());
1046 Ffmpeg localSnaps = ffmpegSnapshot;
1047 if (localSnaps != null) {
1048 localSnaps.startConverting();
1054 public void noMotionDetected(String thisAlarmsChannel) {
1055 setChannelState(thisAlarmsChannel, OnOffType.OFF);
1056 firstMotionAlarm = false;
1057 motionAlarmUpdateSnapshot = false;
1058 motionDetected = false;
1059 if (streamingAutoFps) {
1060 stopSnapshotPolling();
1061 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1062 stopSnapshotPolling();
1067 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
1068 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
1069 * tampering with the camera.
1071 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
1072 updateState(thisAlarmsChannel, state);
1075 public void motionDetected(String thisAlarmsChannel) {
1076 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
1077 updateState(thisAlarmsChannel, OnOffType.ON);
1078 motionDetected = true;
1079 if (streamingAutoFps) {
1080 startSnapshotPolling();
1082 if (cameraConfig.getUpdateImageWhen().contains("2")) {
1083 if (!firstMotionAlarm) {
1084 if (!snapshotUri.isEmpty()) {
1085 sendHttpGET(snapshotUri);
1087 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1089 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1090 if (!snapshotPolling) {
1091 startSnapshotPolling();
1093 firstMotionAlarm = true;
1094 motionAlarmUpdateSnapshot = true;
1098 public void audioDetected() {
1099 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1100 if (cameraConfig.getUpdateImageWhen().contains("3")) {
1101 if (!firstAudioAlarm) {
1102 if (!snapshotUri.isEmpty()) {
1103 sendHttpGET(snapshotUri);
1105 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1107 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1108 firstAudioAlarm = true;
1109 audioAlarmUpdateSnapshot = true;
1113 public void noAudioDetected() {
1114 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1115 firstAudioAlarm = false;
1116 audioAlarmUpdateSnapshot = false;
1119 public void recordMp4(String filename, int seconds) {
1120 mp4Filename = filename;
1121 mp4RecordTime = seconds;
1122 setupFfmpegFormat(FFmpegFormat.RECORD);
1123 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1126 public void recordGif(String filename, int seconds) {
1127 gifFilename = filename;
1128 gifRecordTime = seconds;
1129 if (cameraConfig.getGifPreroll() > 0) {
1130 snapCount = seconds;
1132 setupFfmpegFormat(FFmpegFormat.GIF);
1134 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1137 public String returnValueFromString(String rawString, String searchedString) {
1139 int index = rawString.indexOf(searchedString);
1140 if (index != -1) // -1 means "not found"
1142 result = rawString.substring(index + searchedString.length(), rawString.length());
1143 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1145 return result; // Did not find a carriage return.
1147 return result.substring(0, index);
1150 return ""; // Did not find the String we were searching for
1153 private void sendPTZRequest() {
1154 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1158 public void handleCommand(ChannelUID channelUID, Command command) {
1159 if (command instanceof RefreshType) {
1160 switch (channelUID.getId()) {
1162 if (onvifCamera.supportsPTZ()) {
1163 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1167 if (onvifCamera.supportsPTZ()) {
1168 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1172 if (onvifCamera.supportsPTZ()) {
1173 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1176 case CHANNEL_GOTO_PRESET:
1177 if (onvifCamera.supportsPTZ()) {
1178 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1182 } // caution "REFRESH" can still progress to brand Handlers below the else.
1184 switch (channelUID.getId()) {
1185 case CHANNEL_MP4_HISTORY_LENGTH:
1186 if (DecimalType.ZERO.equals(command)) {
1187 mp4HistoryLength = 0;
1189 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1192 case CHANNEL_GIF_HISTORY_LENGTH:
1193 if (DecimalType.ZERO.equals(command)) {
1194 gifHistoryLength = 0;
1196 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1199 case CHANNEL_FFMPEG_MOTION_CONTROL:
1200 if (OnOffType.ON.equals(command)) {
1201 motionAlarmEnabled = true;
1202 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1203 motionAlarmEnabled = false;
1204 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1206 motionAlarmEnabled = true;
1207 motionThreshold = Double.valueOf(command.toString());
1208 motionThreshold = motionThreshold / 10000;
1210 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1212 case CHANNEL_START_STREAM:
1214 if (OnOffType.ON.equals(command)) {
1215 localHLS = ffmpegHLS;
1216 if (localHLS == null) {
1217 setupFfmpegFormat(FFmpegFormat.HLS);
1218 localHLS = ffmpegHLS;
1220 if (localHLS != null) {
1221 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1222 localHLS.startConverting();
1225 localHLS = ffmpegHLS;
1226 if (localHLS != null) {
1227 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1228 localHLS.setKeepAlive(1);
1232 case CHANNEL_EXTERNAL_MOTION:
1233 if (OnOffType.ON.equals(command)) {
1234 motionDetected(CHANNEL_EXTERNAL_MOTION);
1236 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1239 case CHANNEL_GOTO_PRESET:
1240 if (onvifCamera.supportsPTZ()) {
1241 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1244 case CHANNEL_POLL_IMAGE:
1245 if (OnOffType.ON.equals(command)) {
1246 if (snapshotUri.isEmpty()) {
1247 ffmpegSnapshotGeneration = true;
1248 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1249 updateImageChannel = false;
1251 updateImageChannel = true;
1252 sendHttpGET(snapshotUri);// Allows this to change Image FPS on demand
1255 Ffmpeg localSnaps = ffmpegSnapshot;
1256 if (localSnaps != null) {
1257 localSnaps.stopConverting();
1258 ffmpegSnapshotGeneration = false;
1260 updateImageChannel = false;
1264 if (onvifCamera.supportsPTZ()) {
1265 if (command instanceof IncreaseDecreaseType) {
1266 if (command == IncreaseDecreaseType.INCREASE) {
1267 if (cameraConfig.getPtzContinuous()) {
1268 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1270 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1273 if (cameraConfig.getPtzContinuous()) {
1274 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1276 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1280 } else if (OnOffType.OFF.equals(command)) {
1281 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1284 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1285 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1289 if (onvifCamera.supportsPTZ()) {
1290 if (command instanceof IncreaseDecreaseType) {
1291 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1292 if (cameraConfig.getPtzContinuous()) {
1293 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1295 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1298 if (cameraConfig.getPtzContinuous()) {
1299 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1301 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1305 } else if (OnOffType.OFF.equals(command)) {
1306 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1309 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1310 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1314 if (onvifCamera.supportsPTZ()) {
1315 if (command instanceof IncreaseDecreaseType) {
1316 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1317 if (cameraConfig.getPtzContinuous()) {
1318 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1320 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1323 if (cameraConfig.getPtzContinuous()) {
1324 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1326 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1330 } else if (OnOffType.OFF.equals(command)) {
1331 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1334 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1335 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1340 // commands and refresh now get passed to brand handlers
1341 switch (thing.getThingTypeUID().getId()) {
1343 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1344 amcrestHandler.handleCommand(channelUID, command);
1345 if (lowPriorityRequests.isEmpty()) {
1346 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1350 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1351 dahuaHandler.handleCommand(channelUID, command);
1352 if (lowPriorityRequests.isEmpty()) {
1353 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1356 case DOORBIRD_THING:
1357 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1358 doorBirdHandler.handleCommand(channelUID, command);
1359 if (lowPriorityRequests.isEmpty()) {
1360 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1363 case HIKVISION_THING:
1364 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1365 hikvisionHandler.handleCommand(channelUID, command);
1366 if (lowPriorityRequests.isEmpty()) {
1367 lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1371 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1372 cameraConfig.getPassword());
1373 foscamHandler.handleCommand(channelUID, command);
1374 if (lowPriorityRequests.isEmpty()) {
1375 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1379 InstarHandler instarHandler = new InstarHandler(getHandle());
1380 instarHandler.handleCommand(channelUID, command);
1381 if (lowPriorityRequests.isEmpty()) {
1382 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1386 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1387 defaultHandler.handleCommand(channelUID, command);
1388 if (lowPriorityRequests.isEmpty()) {
1389 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1395 public void setChannelState(String channelToUpdate, State valueOf) {
1396 updateState(channelToUpdate, valueOf);
1399 void bringCameraOnline() {
1401 updateStatus(ThingStatus.ONLINE);
1402 groupTracker.listOfOnlineCameraHandlers.add(this);
1403 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1404 Future<?> localFuture = cameraConnectionJob;
1405 if (localFuture != null) {
1406 localFuture.cancel(false);
1409 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1410 snapshotPolling = true;
1411 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 1000, cameraConfig.getPollTime(),
1412 TimeUnit.MILLISECONDS);
1415 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1417 if (!rtspUri.isEmpty()) {
1418 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1420 if (updateImageChannel) {
1421 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1423 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1425 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1426 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1427 handle.cameraOnline(getThing().getUID().getId());
1432 void snapshotIsFfmpeg() {
1433 bringCameraOnline();
1434 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1436 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1437 if (!rtspUri.isEmpty()) {
1438 updateImageChannel = false;
1439 ffmpegSnapshotGeneration = true;
1440 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1441 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1443 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1447 void pollingCameraConnection() {
1448 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1449 if (rtspUri.isEmpty()) {
1450 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1452 if (snapshotUri.isEmpty() || snapshotUri.equals("ffmpeg")) {
1455 sendHttpRequest("GET", snapshotUri, null);
1459 if (!onvifCamera.isConnected()) {
1460 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1461 cameraConfig.getOnvifPort());
1462 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1464 if (snapshotUri.equals("ffmpeg")) {
1466 } else if (!snapshotUri.isEmpty()) {
1467 sendHttpRequest("GET", snapshotUri, null);
1468 } else if (!rtspUri.isEmpty()) {
1471 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1472 "Camera failed to report a valid Snaphot and/or RTSP URL. See readme on how to use the SNAPSHOT_URL_OVERRIDE feature.");
1476 public void cameraConfigError(String reason) {
1477 // wont try to reconnect again due to a config error being the cause.
1478 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1482 public void cameraCommunicationError(String reason) {
1483 // will try to reconnect again as camera may be rebooting.
1484 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1485 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1486 resetAndRetryConnecting();
1490 boolean streamIsStopped(String url) {
1491 ChannelTracking channelTracking = channelTrackingMap.get(url);
1492 if (channelTracking != null) {
1493 if (channelTracking.getChannel().isActive()) {
1494 return false; // stream is running.
1497 return true; // Stream stopped or never started.
1500 void snapshotRunnable() {
1501 // Snapshot should be first to keep consistent time between shots
1502 sendHttpGET(snapshotUri);
1503 if (snapCount > 0) {
1504 if (--snapCount == 0) {
1505 setupFfmpegFormat(FFmpegFormat.GIF);
1510 public void stopSnapshotPolling() {
1511 Future<?> localFuture;
1512 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1513 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1514 snapshotPolling = false;
1515 localFuture = snapshotJob;
1516 if (localFuture != null) {
1517 localFuture.cancel(true);
1519 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1520 snapshotPolling = false;
1521 localFuture = snapshotJob;
1522 if (localFuture != null) {
1523 localFuture.cancel(true);
1528 public void startSnapshotPolling() {
1529 if (snapshotPolling || ffmpegSnapshotGeneration) {
1530 return; // Already polling or creating with FFmpeg from RTSP
1532 if (streamingSnapshotMjpeg || streamingAutoFps) {
1533 snapshotPolling = true;
1534 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1535 TimeUnit.MILLISECONDS);
1536 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1537 snapshotPolling = true;
1538 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1539 TimeUnit.MILLISECONDS);
1544 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep mjpeg and alarm
1545 * streams open and more.
1548 void pollCameraRunnable() {
1549 // Snapshot should be first to keep consistent time between shots
1550 if (streamingAutoFps) {
1551 updateAutoFps = true;
1552 if (!snapshotPolling && !ffmpegSnapshotGeneration) {
1553 // Dont need to poll if creating from RTSP stream with FFmpeg or we are polling at full rate already.
1554 sendHttpGET(snapshotUri);
1556 } else if (!snapshotUri.isEmpty() && !snapshotPolling) {// we need to check camera is still online.
1557 sendHttpGET(snapshotUri);
1559 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1560 if (!lowPriorityRequests.isEmpty()) {
1561 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1562 lowPriorityCounter = 0;
1564 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1566 // what needs to be done every poll//
1567 switch (thing.getThingTypeUID().getId()) {
1571 if (!onvifCamera.isConnected()) {
1572 onvifCamera.connect(true);
1576 noMotionDetected(CHANNEL_MOTION_ALARM);
1577 noMotionDetected(CHANNEL_PIR_ALARM);
1580 case HIKVISION_THING:
1581 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1582 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1583 cameraConfig.getIp());
1584 sendHttpGET("/ISAPI/Event/notification/alertStream");
1588 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1589 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1592 // Check for alarms, channel for NVRs appears not to work at filtering.
1593 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1594 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1595 cameraConfig.getIp());
1596 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1599 case DOORBIRD_THING:
1600 // Check for alarms, channel for NVRs appears not to work at filtering.
1601 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1602 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1603 cameraConfig.getIp());
1604 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1608 Ffmpeg localHLS = ffmpegHLS;
1609 if (localHLS != null) {
1610 localHLS.checkKeepAlive();
1612 if (openChannels.size() > 18) {
1613 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1619 public void initialize() {
1620 cameraConfig = getConfigAs(CameraConfig.class);
1621 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1622 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1623 rtspUri = cameraConfig.getFfmpegInput();
1625 if (cameraConfig.getServerPort() < 1) {
1627 "The Server Port is not set to a valid number which disables a lot of binding features. See readme for more info.");
1628 } else if (cameraConfig.getServerPort() < 1025) {
1629 logger.warn("The Server Port is <= 1024 and may cause permission errors under Linux, try a higher number.");
1632 // Known cameras will connect quicker if we skip ONVIF questions.
1633 switch (thing.getThingTypeUID().getId()) {
1636 if (mjpegUri.isEmpty()) {
1637 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1639 if (snapshotUri.isEmpty()) {
1640 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1643 case DOORBIRD_THING:
1644 if (mjpegUri.isEmpty()) {
1645 mjpegUri = "/bha-api/video.cgi";
1647 if (snapshotUri.isEmpty()) {
1648 snapshotUri = "/bha-api/image.cgi";
1652 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1653 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1654 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1655 if (mjpegUri.isEmpty()) {
1656 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1657 + cameraConfig.getPassword();
1659 if (snapshotUri.isEmpty()) {
1660 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1661 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1664 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1665 if (mjpegUri.isEmpty()) {
1666 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1668 if (snapshotUri.isEmpty()) {
1669 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1673 if (snapshotUri.isEmpty()) {
1674 snapshotUri = "/tmpfs/snap.jpg";
1676 if (mjpegUri.isEmpty()) {
1677 mjpegUri = "/mjpegstream.cgi?-chn=12";
1682 // Onvif and Instar event handling needs the host IP and the server started.
1683 if (cameraConfig.getServerPort() > 0) {
1684 startStreamServer();
1687 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1688 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1689 cameraConfig.getUser(), cameraConfig.getPassword());
1690 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1691 // Only use ONVIF events if it is not an API camera.
1692 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1695 // for poll times above 9 seconds don't display a warning about the Image channel.
1696 if (9000 <= cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1698 "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.");
1700 // Waiting 3 seconds for ONVIF to discover the urls before running.
1701 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 30, TimeUnit.SECONDS);
1704 // What the camera needs to re-connect if the initialize() is not called.
1705 private void resetAndRetryConnecting() {
1711 public void dispose() {
1713 snapshotPolling = false;
1714 onvifCamera.disconnect();
1715 Future<?> localFuture = pollCameraJob;
1716 if (localFuture != null) {
1717 localFuture.cancel(true);
1719 localFuture = snapshotJob;
1720 if (localFuture != null) {
1721 localFuture.cancel(true);
1723 localFuture = cameraConnectionJob;
1724 if (localFuture != null) {
1725 localFuture.cancel(true);
1727 threadPool.shutdown();
1728 threadPool = Executors.newScheduledThreadPool(4);
1730 groupTracker.listOfOnlineCameraHandlers.remove(this);
1731 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1732 // inform all group handlers that this camera has gone offline
1733 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1734 handle.cameraOffline(this);
1736 basicAuth = ""; // clear out stored Password hash
1737 useDigestAuth = false;
1739 openChannels.close();
1741 Ffmpeg localFfmpeg = ffmpegHLS;
1742 if (localFfmpeg != null) {
1743 localFfmpeg.stopConverting();
1745 localFfmpeg = ffmpegRecord;
1746 if (localFfmpeg != null) {
1747 localFfmpeg.stopConverting();
1749 localFfmpeg = ffmpegGIF;
1750 if (localFfmpeg != null) {
1751 localFfmpeg.stopConverting();
1753 localFfmpeg = ffmpegRtspHelper;
1754 if (localFfmpeg != null) {
1755 localFfmpeg.stopConverting();
1757 localFfmpeg = ffmpegMjpeg;
1758 if (localFfmpeg != null) {
1759 localFfmpeg.stopConverting();
1761 localFfmpeg = ffmpegSnapshot;
1762 if (localFfmpeg != null) {
1763 localFfmpeg.stopConverting();
1765 channelTrackingMap.clear();
1768 public void setStreamServerHandler(StreamServerHandler streamServerHandler2) {
1769 streamServerHandler = streamServerHandler2;
1772 public String getWhiteList() {
1773 return cameraConfig.getIpWhitelist();
1777 public Collection<Class<? extends ThingHandlerService>> getServices() {
1778 return Collections.singleton(IpCameraActions.class);