2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.ipcamera.internal.handler;
15 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
18 import java.io.FileNotFoundException;
19 import java.io.FileOutputStream;
20 import java.io.IOException;
21 import java.io.OutputStream;
22 import java.net.InetSocketAddress;
23 import java.net.MalformedURLException;
25 import java.nio.charset.StandardCharsets;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.LinkedList;
30 import java.util.List;
32 import java.util.concurrent.ConcurrentHashMap;
33 import java.util.concurrent.Executors;
34 import java.util.concurrent.Future;
35 import java.util.concurrent.ScheduledExecutorService;
36 import java.util.concurrent.ScheduledFuture;
37 import java.util.concurrent.TimeUnit;
38 import java.util.concurrent.locks.ReentrantLock;
40 import org.eclipse.jdt.annotation.NonNullByDefault;
41 import org.eclipse.jdt.annotation.Nullable;
42 import org.openhab.binding.ipcamera.internal.AmcrestHandler;
43 import org.openhab.binding.ipcamera.internal.CameraConfig;
44 import org.openhab.binding.ipcamera.internal.ChannelTracking;
45 import org.openhab.binding.ipcamera.internal.DahuaHandler;
46 import org.openhab.binding.ipcamera.internal.DoorBirdHandler;
47 import org.openhab.binding.ipcamera.internal.Ffmpeg;
48 import org.openhab.binding.ipcamera.internal.FoscamHandler;
49 import org.openhab.binding.ipcamera.internal.GroupTracker;
50 import org.openhab.binding.ipcamera.internal.Helper;
51 import org.openhab.binding.ipcamera.internal.HikvisionHandler;
52 import org.openhab.binding.ipcamera.internal.HttpOnlyHandler;
53 import org.openhab.binding.ipcamera.internal.InstarHandler;
54 import org.openhab.binding.ipcamera.internal.IpCameraActions;
55 import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
56 import org.openhab.binding.ipcamera.internal.IpCameraDynamicStateDescriptionProvider;
57 import org.openhab.binding.ipcamera.internal.MyNettyAuthHandler;
58 import org.openhab.binding.ipcamera.internal.StreamServerHandler;
59 import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection;
60 import org.openhab.core.library.types.DecimalType;
61 import org.openhab.core.library.types.IncreaseDecreaseType;
62 import org.openhab.core.library.types.OnOffType;
63 import org.openhab.core.library.types.PercentType;
64 import org.openhab.core.library.types.RawType;
65 import org.openhab.core.library.types.StringType;
66 import org.openhab.core.thing.ChannelUID;
67 import org.openhab.core.thing.Thing;
68 import org.openhab.core.thing.ThingStatus;
69 import org.openhab.core.thing.ThingStatusDetail;
70 import org.openhab.core.thing.binding.BaseThingHandler;
71 import org.openhab.core.thing.binding.ThingHandlerService;
72 import org.openhab.core.types.Command;
73 import org.openhab.core.types.RefreshType;
74 import org.openhab.core.types.State;
75 import org.slf4j.Logger;
76 import org.slf4j.LoggerFactory;
78 import io.netty.bootstrap.Bootstrap;
79 import io.netty.bootstrap.ServerBootstrap;
80 import io.netty.buffer.ByteBuf;
81 import io.netty.buffer.Unpooled;
82 import io.netty.channel.Channel;
83 import io.netty.channel.ChannelDuplexHandler;
84 import io.netty.channel.ChannelFuture;
85 import io.netty.channel.ChannelFutureListener;
86 import io.netty.channel.ChannelHandlerContext;
87 import io.netty.channel.ChannelInitializer;
88 import io.netty.channel.ChannelOption;
89 import io.netty.channel.EventLoopGroup;
90 import io.netty.channel.group.ChannelGroup;
91 import io.netty.channel.group.DefaultChannelGroup;
92 import io.netty.channel.nio.NioEventLoopGroup;
93 import io.netty.channel.socket.SocketChannel;
94 import io.netty.channel.socket.nio.NioServerSocketChannel;
95 import io.netty.channel.socket.nio.NioSocketChannel;
96 import io.netty.handler.codec.base64.Base64;
97 import io.netty.handler.codec.http.DefaultFullHttpRequest;
98 import io.netty.handler.codec.http.DefaultHttpResponse;
99 import io.netty.handler.codec.http.FullHttpRequest;
100 import io.netty.handler.codec.http.HttpClientCodec;
101 import io.netty.handler.codec.http.HttpContent;
102 import io.netty.handler.codec.http.HttpHeaderNames;
103 import io.netty.handler.codec.http.HttpHeaderValues;
104 import io.netty.handler.codec.http.HttpMessage;
105 import io.netty.handler.codec.http.HttpMethod;
106 import io.netty.handler.codec.http.HttpResponse;
107 import io.netty.handler.codec.http.HttpResponseStatus;
108 import io.netty.handler.codec.http.HttpServerCodec;
109 import io.netty.handler.codec.http.HttpVersion;
110 import io.netty.handler.codec.http.LastHttpContent;
111 import io.netty.handler.stream.ChunkedWriteHandler;
112 import io.netty.handler.timeout.IdleState;
113 import io.netty.handler.timeout.IdleStateEvent;
114 import io.netty.handler.timeout.IdleStateHandler;
115 import io.netty.util.CharsetUtil;
116 import io.netty.util.ReferenceCountUtil;
117 import io.netty.util.concurrent.GlobalEventExecutor;
120 * The {@link IpCameraHandler} is responsible for handling commands, which are
121 * sent to one of the channels.
123 * @author Matthew Skinner - Initial contribution
127 public class IpCameraHandler extends BaseThingHandler {
128 public final Logger logger = LoggerFactory.getLogger(getClass());
129 public final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
130 private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(4);
131 private GroupTracker groupTracker;
132 public CameraConfig cameraConfig = new CameraConfig();
134 // ChannelGroup is thread safe
135 public final ChannelGroup mjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
136 private final ChannelGroup snapshotMjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
137 private final ChannelGroup autoSnapshotMjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
138 public final ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
139 public @Nullable Ffmpeg ffmpegHLS = null;
140 public @Nullable Ffmpeg ffmpegRecord = null;
141 public @Nullable Ffmpeg ffmpegGIF = null;
142 public @Nullable Ffmpeg ffmpegRtspHelper = null;
143 public @Nullable Ffmpeg ffmpegMjpeg = null;
144 public @Nullable Ffmpeg ffmpegSnapshot = null;
145 public boolean streamingAutoFps = false;
146 public boolean motionDetected = false;
148 private @Nullable ScheduledFuture<?> cameraConnectionJob = null;
149 private @Nullable ScheduledFuture<?> pollCameraJob = null;
150 private @Nullable ScheduledFuture<?> snapshotJob = null;
151 private @Nullable Bootstrap mainBootstrap;
152 private @Nullable ServerBootstrap serverBootstrap;
154 private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup();
155 private EventLoopGroup serversLoopGroup = new NioEventLoopGroup();
156 private FullHttpRequest putRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
158 private String gifFilename = "ipcamera";
159 private String gifHistory = "";
160 private String mp4History = "";
161 public int gifHistoryLength;
162 public int mp4HistoryLength;
163 private String mp4Filename = "ipcamera";
164 private int mp4RecordTime;
165 private int gifRecordTime = 5;
166 private LinkedList<byte[]> fifoSnapshotBuffer = new LinkedList<byte[]>();
167 private int snapCount;
168 private boolean updateImageChannel = false;
169 private boolean updateAutoFps = false;
170 private byte lowPriorityCounter = 0;
171 public String hostIp;
172 public Map<String, ChannelTracking> channelTrackingMap = new ConcurrentHashMap<>();
173 public List<String> lowPriorityRequests = new ArrayList<>(0);
175 // basicAuth MUST remain private as it holds the cameraConfig.getPassword()
176 private String basicAuth = "";
177 public boolean useBasicAuth = false;
178 public boolean useDigestAuth = false;
179 public String snapshotUri = "";
180 public String mjpegUri = "";
181 private @Nullable ChannelFuture serverFuture = null;
182 private Object firstStreamedMsg = new Object();
183 public byte[] currentSnapshot = new byte[] { (byte) 0x00 };
184 public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
185 public String rtspUri = "";
186 public boolean audioAlarmUpdateSnapshot = false;
187 private boolean motionAlarmUpdateSnapshot = false;
188 private boolean isOnline = false; // Used so only 1 error is logged when a network issue occurs.
189 private boolean firstAudioAlarm = false;
190 private boolean firstMotionAlarm = false;
191 public Double motionThreshold = 0.0016;
192 public int audioThreshold = 35;
193 @SuppressWarnings("unused")
194 private @Nullable StreamServerHandler streamServerHandler;
195 private boolean streamingSnapshotMjpeg = false;
196 public boolean motionAlarmEnabled = false;
197 public boolean audioAlarmEnabled = false;
198 public boolean ffmpegSnapshotGeneration = false;
199 public boolean snapshotPolling = false;
200 public OnvifConnection onvifCamera = new OnvifConnection(this, "", "", "");
202 // These methods handle the response from all camera brands, nothing specific to 1 brand.
203 private class CommonCameraHandler extends ChannelDuplexHandler {
204 private int bytesToRecieve = 0;
205 private int bytesAlreadyRecieved = 0;
206 private byte[] incomingJpeg = new byte[0];
207 private String incomingMessage = "";
208 private String contentType = "empty";
209 private Object reply = new Object();
210 private String requestUrl = "";
211 private boolean closeConnection = true;
212 private boolean isChunked = false;
214 public void setURL(String url) {
219 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
220 if (msg == null || ctx == null) {
224 if (msg instanceof HttpResponse) {
225 HttpResponse response = (HttpResponse) msg;
226 if (response.status().code() != 401) {
227 if (!response.headers().isEmpty()) {
228 for (String name : response.headers().names()) {
229 // Some cameras use first letter uppercase and others dont.
230 switch (name.toLowerCase()) { // Possible localization issues doing this
232 contentType = response.headers().getAsString(name);
234 case "content-length":
235 bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
238 if (response.headers().getAsString(name).contains("keep-alive")) {
239 closeConnection = false;
242 case "transfer-encoding":
243 if (response.headers().getAsString(name).contains("chunked")) {
249 if (contentType.contains("multipart")) {
250 closeConnection = false;
251 if (mjpegUri.equals(requestUrl)) {
252 if (msg instanceof HttpMessage) {
253 // very start of stream only
254 ReferenceCountUtil.retain(msg, 1);
255 firstStreamedMsg = msg;
256 streamToGroup(firstStreamedMsg, mjpegChannelGroup, true);
259 } else if (contentType.contains("image/jp")) {
260 if (bytesToRecieve == 0) {
261 bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
262 logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
264 incomingJpeg = new byte[bytesToRecieve];
269 if (msg instanceof HttpContent) {
270 if (mjpegUri.equals(requestUrl)) {
271 // multiple MJPEG stream packets come back as this.
272 ReferenceCountUtil.retain(msg, 1);
273 streamToGroup(msg, mjpegChannelGroup, true);
275 HttpContent content = (HttpContent) msg;
276 // Found some cameras use Content-Type: image/jpg instead of image/jpeg
277 if (contentType.contains("image/jp")) {
278 for (int i = 0; i < content.content().capacity(); i++) {
279 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
281 if (content instanceof LastHttpContent) {
282 processSnapshot(incomingJpeg);
283 // testing next line and if works need to do a full cleanup of this function.
284 closeConnection = true;
285 if (closeConnection) {
289 bytesAlreadyRecieved = 0;
292 } else { // incomingMessage that is not an IMAGE
293 if (incomingMessage.isEmpty()) {
294 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
296 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
298 bytesAlreadyRecieved = incomingMessage.length();
299 if (content instanceof LastHttpContent) {
300 // If it is not an image send it on to the next handler//
301 if (bytesAlreadyRecieved != 0) {
302 reply = incomingMessage;
303 super.channelRead(ctx, reply);
306 // Alarm Streams never have a LastHttpContent as they always stay open//
307 else if (contentType.contains("multipart")) {
308 if (bytesAlreadyRecieved != 0) {
309 reply = incomingMessage;
310 incomingMessage = "";
312 bytesAlreadyRecieved = 0;
313 super.channelRead(ctx, reply);
316 // Foscam needs this as will other cameras with chunks//
317 if (isChunked && bytesAlreadyRecieved != 0) {
318 logger.debug("Reply is chunked.");
319 reply = incomingMessage;
320 super.channelRead(ctx, reply);
324 } else { // msg is not HttpContent
325 // Foscam cameras need this
326 if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
327 reply = incomingMessage;
328 logger.debug("Packet back from camera is {}", incomingMessage);
329 super.channelRead(ctx, reply);
333 ReferenceCountUtil.release(msg);
338 public void channelReadComplete(@Nullable ChannelHandlerContext ctx) {
342 public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
346 public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
350 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
351 if (cause == null || ctx == null) {
354 if (cause instanceof ArrayIndexOutOfBoundsException) {
355 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
358 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
365 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
369 if (evt instanceof IdleStateEvent) {
370 IdleStateEvent e = (IdleStateEvent) evt;
371 // If camera does not use the channel for X amount of time it will close.
372 if (e.state() == IdleState.READER_IDLE) {
373 String urlToKeepOpen = "";
374 switch (thing.getThingTypeUID().getId()) {
376 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
378 case HIKVISION_THING:
379 urlToKeepOpen = "/ISAPI/Event/notification/alertStream";
382 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
385 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
386 if (channelTracking != null) {
387 if (channelTracking.getChannel() == ctx.channel()) {
388 return; // don't auto close this as it is for the alarms.
397 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
398 IpCameraDynamicStateDescriptionProvider stateDescriptionProvider) {
400 this.stateDescriptionProvider = stateDescriptionProvider;
401 if (ipAddress != null) {
404 hostIp = Helper.getLocalIpAddress();
406 this.groupTracker = groupTracker;
409 private IpCameraHandler getHandle() {
413 // false clears the stored user/pass hash, true creates the hash
414 public boolean setBasicAuth(boolean useBasic) {
416 logger.debug("Clearing out the stored BASIC auth now.");
419 } else if (!basicAuth.isEmpty()) {
420 // due to camera may have been sent multiple requests before the auth was set, this may trigger falsely.
421 logger.warn("Camera is reporting your username and/or password is wrong.");
424 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
425 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
426 ByteBuf byteBuf = null;
428 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
429 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
431 if (byteBuf != null) {
437 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
442 private String getCorrectUrlFormat(String longUrl) {
443 String temp = longUrl;
446 if (longUrl.isEmpty() || longUrl.equals("ffmpeg")) {
451 url = new URL(longUrl);
452 int port = url.getPort();
454 if (url.getQuery() == null) {
455 temp = url.getPath();
457 temp = url.getPath() + "?" + url.getQuery();
460 if (url.getQuery() == null) {
461 temp = ":" + url.getPort() + url.getPath();
463 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
466 } catch (MalformedURLException e) {
467 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
472 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
473 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
474 sendHttpRequest("PUT", httpRequestURL, null);
477 public void sendHttpGET(String httpRequestURL) {
478 sendHttpRequest("GET", httpRequestURL, null);
481 public int getPortFromShortenedUrl(String httpRequestURL) {
482 if (httpRequestURL.startsWith(":")) {
483 int end = httpRequestURL.indexOf("/");
484 return Integer.parseInt(httpRequestURL.substring(1, end));
486 return cameraConfig.getPort();
489 public String getTinyUrl(String httpRequestURL) {
490 if (httpRequestURL.startsWith(":")) {
491 int beginIndex = httpRequestURL.indexOf("/");
492 return httpRequestURL.substring(beginIndex);
494 return httpRequestURL;
497 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
498 // The authHandler will generate a digest string and re-send using this same function when needed.
499 @SuppressWarnings("null")
500 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
501 int port = getPortFromShortenedUrl(httpRequestURLFull);
502 String httpRequestURL = getTinyUrl(httpRequestURLFull);
504 if (mainBootstrap == null) {
505 mainBootstrap = new Bootstrap();
506 mainBootstrap.group(mainEventLoopGroup);
507 mainBootstrap.channel(NioSocketChannel.class);
508 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
509 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
510 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
511 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
512 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
513 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
516 public void initChannel(SocketChannel socketChannel) throws Exception {
517 // HIK Alarm stream needs > 9sec idle to stop stream closing
518 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
519 socketChannel.pipeline().addLast(new HttpClientCodec());
520 socketChannel.pipeline().addLast(AUTH_HANDLER,
521 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
522 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
524 switch (thing.getThingTypeUID().getId()) {
526 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
529 socketChannel.pipeline()
530 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
533 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
536 socketChannel.pipeline().addLast(
537 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
539 case HIKVISION_THING:
540 socketChannel.pipeline()
541 .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
544 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
547 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
554 FullHttpRequest request;
555 if (!"PUT".equals(httpMethod) || (useDigestAuth && digestString == null)) {
556 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
557 request.headers().set("Host", cameraConfig.getIp() + ":" + port);
558 request.headers().set("Connection", HttpHeaderValues.KEEP_ALIVE);
560 request = putRequestWithBody;
563 if (!basicAuth.isEmpty()) {
565 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
568 request.headers().set("Authorization", "Basic " + basicAuth);
573 if (digestString != null) {
574 request.headers().set("Authorization", "Digest " + digestString);
578 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
579 .addListener(new ChannelFutureListener() {
582 public void operationComplete(@Nullable ChannelFuture future) {
583 if (future == null) {
586 if (future.isDone() && future.isSuccess()) {
587 Channel ch = future.channel();
588 openChannels.add(ch);
592 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
594 channelTrackingMap.put(httpRequestURL, new ChannelTracking(ch, httpRequestURL));
596 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
597 commonHandler.setURL(httpRequestURLFull);
598 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
599 authHandler.setURL(httpMethod, httpRequestURL);
601 switch (thing.getThingTypeUID().getId()) {
603 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
604 amcrestHandler.setURL(httpRequestURL);
607 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
608 instarHandler.setURL(httpRequestURL);
611 ch.writeAndFlush(request);
612 } else { // an error occured
613 cameraCommunicationError(
614 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
620 public void processSnapshot(byte[] incommingSnapshot) {
621 lockCurrentSnapshot.lock();
623 currentSnapshot = incommingSnapshot;
624 if (cameraConfig.getGifPreroll() > 0) {
625 fifoSnapshotBuffer.add(incommingSnapshot);
626 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
627 fifoSnapshotBuffer.removeFirst();
631 lockCurrentSnapshot.unlock();
634 if (streamingSnapshotMjpeg) {
635 sendMjpegFrame(incommingSnapshot, snapshotMjpegChannelGroup);
637 if (streamingAutoFps) {
638 if (motionDetected) {
639 sendMjpegFrame(incommingSnapshot, autoSnapshotMjpegChannelGroup);
640 } else if (updateAutoFps) {
641 // only happens every 8 seconds as some browsers need a frame that often to keep stream alive.
642 sendMjpegFrame(incommingSnapshot, autoSnapshotMjpegChannelGroup);
643 updateAutoFps = false;
647 if (updateImageChannel) {
648 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
649 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
650 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
651 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
652 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
653 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
654 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
658 public void stopStreamServer() {
659 serversLoopGroup.shutdownGracefully();
660 serverBootstrap = null;
663 @SuppressWarnings("null")
664 public void startStreamServer() {
665 if (serverBootstrap == null) {
667 serversLoopGroup = new NioEventLoopGroup();
668 serverBootstrap = new ServerBootstrap();
669 serverBootstrap.group(serversLoopGroup);
670 serverBootstrap.channel(NioServerSocketChannel.class);
671 // IP "0.0.0.0" will bind the server to all network connections//
672 serverBootstrap.localAddress(new InetSocketAddress("0.0.0.0", cameraConfig.getServerPort()));
673 serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
675 protected void initChannel(SocketChannel socketChannel) throws Exception {
676 socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 60, 0));
677 socketChannel.pipeline().addLast("HttpServerCodec", new HttpServerCodec());
678 socketChannel.pipeline().addLast("ChunkedWriteHandler", new ChunkedWriteHandler());
679 socketChannel.pipeline().addLast("streamServerHandler", new StreamServerHandler(getHandle()));
682 serverFuture = serverBootstrap.bind().sync();
683 serverFuture.await(4000);
684 logger.debug("File server for camera at {} has started on port {} for all NIC's.", cameraConfig.getIp(),
685 cameraConfig.getServerPort());
686 updateState(CHANNEL_MJPEG_URL,
687 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.mjpeg"));
688 updateState(CHANNEL_HLS_URL,
689 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.m3u8"));
690 updateState(CHANNEL_IMAGE_URL,
691 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.jpg"));
692 } catch (Exception e) {
693 cameraConfigError("Exception when starting server. Try changing the Server Port to another number.");
695 if (thing.getThingTypeUID().getId().equals(INSTAR_THING)) {
696 logger.debug("Setting up the Alarm Server settings in the camera now");
698 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
699 + hostIp + "&-as_port=" + cameraConfig.getServerPort()
700 + "&-as_path=/instar&-as_queryattr1=&-as_queryval1=&-as_queryattr2=&-as_queryval2=&-as_queryattr3=&-as_queryval3=&-as_activequery=1&-as_auth=0&-as_query1=0&-as_query2=0&-as_query3=0");
705 public void setupSnapshotStreaming(boolean stream, ChannelHandlerContext ctx, boolean auto) {
707 sendMjpegFirstPacket(ctx);
709 autoSnapshotMjpegChannelGroup.add(ctx.channel());
710 lockCurrentSnapshot.lock();
712 sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
713 // iOS uses a FIFO? and needs two frames to display a pic
714 sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
716 lockCurrentSnapshot.unlock();
718 streamingAutoFps = true;
720 snapshotMjpegChannelGroup.add(ctx.channel());
721 lockCurrentSnapshot.lock();
723 sendMjpegFrame(currentSnapshot, snapshotMjpegChannelGroup);
725 lockCurrentSnapshot.unlock();
727 streamingSnapshotMjpeg = true;
728 startSnapshotPolling();
731 snapshotMjpegChannelGroup.remove(ctx.channel());
732 autoSnapshotMjpegChannelGroup.remove(ctx.channel());
733 if (streamingSnapshotMjpeg && snapshotMjpegChannelGroup.isEmpty()) {
734 streamingSnapshotMjpeg = false;
735 stopSnapshotPolling();
736 logger.debug("All snapshots.mjpeg streams have stopped.");
737 } else if (streamingAutoFps && autoSnapshotMjpegChannelGroup.isEmpty()) {
738 streamingAutoFps = false;
739 stopSnapshotPolling();
740 logger.debug("All autofps.mjpeg streams have stopped.");
745 // If start is true the CTX is added to the list to stream video to, false stops
747 public void setupMjpegStreaming(boolean start, ChannelHandlerContext ctx) {
749 if (mjpegChannelGroup.isEmpty()) {// first stream being requested.
750 mjpegChannelGroup.add(ctx.channel());
751 if (mjpegUri.isEmpty() || mjpegUri.equals("ffmpeg")) {
752 sendMjpegFirstPacket(ctx);
753 setupFfmpegFormat(FFmpegFormat.MJPEG);
756 // fix Dahua reboots when refreshing a mjpeg stream.
757 TimeUnit.MILLISECONDS.sleep(500);
758 } catch (InterruptedException e) {
760 sendHttpGET(mjpegUri);
762 } else if (ffmpegMjpeg != null) {// not first stream and we will use ffmpeg
763 sendMjpegFirstPacket(ctx);
764 mjpegChannelGroup.add(ctx.channel());
765 } else {// not first stream and camera supplies the mjpeg source.
766 ctx.channel().writeAndFlush(firstStreamedMsg);
767 mjpegChannelGroup.add(ctx.channel());
770 mjpegChannelGroup.remove(ctx.channel());
771 if (mjpegChannelGroup.isEmpty()) {
772 logger.debug("All ipcamera.mjpeg streams have stopped.");
773 if (mjpegUri.equals("ffmpeg") || mjpegUri.isEmpty()) {
774 Ffmpeg localMjpeg = ffmpegMjpeg;
775 if (localMjpeg != null) {
776 localMjpeg.stopConverting();
779 closeChannel(getTinyUrl(mjpegUri));
785 void closeChannel(String url) {
786 ChannelTracking channelTracking = channelTrackingMap.get(url);
787 if (channelTracking != null) {
788 if (channelTracking.getChannel().isOpen()) {
789 channelTracking.getChannel().close();
796 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
797 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
800 void cleanChannels() {
801 for (Channel channel : openChannels) {
802 boolean oldChannel = true;
803 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
804 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
805 channelTrackingMap.remove(channelTracking.getRequestUrl());
807 if (channelTracking.getChannel() == channel) {
808 logger.trace("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
818 public void storeHttpReply(String url, String content) {
819 ChannelTracking channelTracking = channelTrackingMap.get(url);
820 if (channelTracking != null) {
821 channelTracking.setReply(content);
825 // sends direct to ctx so can be either snapshots.mjpeg or normal mjpeg stream
826 public void sendMjpegFirstPacket(ChannelHandlerContext ctx) {
827 final String boundary = "thisMjpegStream";
828 String contentType = "multipart/x-mixed-replace; boundary=" + boundary;
829 HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
830 response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
831 response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
832 response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
833 response.headers().add("Access-Control-Allow-Origin", "*");
834 response.headers().add("Access-Control-Expose-Headers", "*");
835 ctx.channel().writeAndFlush(response);
838 public void sendMjpegFrame(byte[] jpg, ChannelGroup channelGroup) {
839 final String boundary = "thisMjpegStream";
840 ByteBuf imageByteBuf = Unpooled.copiedBuffer(jpg);
841 int length = imageByteBuf.readableBytes();
842 String header = "--" + boundary + "\r\n" + "content-type: image/jpeg" + "\r\n" + "content-length: " + length
844 ByteBuf headerBbuf = Unpooled.copiedBuffer(header, 0, header.length(), StandardCharsets.UTF_8);
845 ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
846 streamToGroup(headerBbuf, channelGroup, false);
847 streamToGroup(imageByteBuf, channelGroup, false);
848 streamToGroup(footerBbuf, channelGroup, true);
851 public void streamToGroup(Object msg, ChannelGroup channelGroup, boolean flush) {
852 channelGroup.write(msg);
854 channelGroup.flush();
858 private void storeSnapshots() {
860 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
861 lockCurrentSnapshot.lock();
863 for (byte[] foo : fifoSnapshotBuffer) {
864 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
867 OutputStream fos = new FileOutputStream(file);
870 } catch (FileNotFoundException e) {
871 logger.warn("FileNotFoundException {}", e.getMessage());
872 } catch (IOException e) {
873 logger.warn("IOException {}", e.getMessage());
877 lockCurrentSnapshot.unlock();
881 public void setupFfmpegFormat(FFmpegFormat format) {
882 String inputOptions = cameraConfig.getFfmpegInputOptions();
883 if (cameraConfig.getFfmpegOutput().isEmpty()) {
884 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
887 if (rtspUri.isEmpty()) {
888 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
891 if (cameraConfig.getFfmpegLocation().isEmpty()) {
892 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
895 if (rtspUri.toLowerCase().contains("rtsp")) {
896 if (inputOptions.isEmpty()) {
897 inputOptions = "-rtsp_transport tcp";
901 // Make sure the folder exists, if not create it.
902 new File(cameraConfig.getFfmpegOutput()).mkdirs();
905 if (ffmpegHLS == null) {
906 if (!inputOptions.isEmpty()) {
907 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
908 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
909 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
910 cameraConfig.getUser(), cameraConfig.getPassword());
912 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
913 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
914 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
915 cameraConfig.getPassword());
918 Ffmpeg localHLS = ffmpegHLS;
919 if (localHLS != null) {
920 localHLS.startConverting();
924 if (cameraConfig.getGifPreroll() > 0) {
925 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
926 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
927 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
928 + cameraConfig.getGifOutOptions(),
929 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
930 cameraConfig.getPassword());
932 if (!inputOptions.isEmpty()) {
933 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
935 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
937 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
938 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
939 cameraConfig.getUser(), cameraConfig.getPassword());
941 if (cameraConfig.getGifPreroll() > 0) {
944 Ffmpeg localGIF = ffmpegGIF;
945 if (localGIF != null) {
946 localGIF.startConverting();
947 if (gifHistory.isEmpty()) {
948 gifHistory = gifFilename;
949 } else if (!gifFilename.equals("ipcamera")) {
950 gifHistory = gifFilename + "," + gifHistory;
951 if (gifHistoryLength > 49) {
952 int endIndex = gifHistory.lastIndexOf(",");
953 gifHistory = gifHistory.substring(0, endIndex);
956 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
960 if (!inputOptions.isEmpty()) {
961 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
963 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
965 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
966 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
967 cameraConfig.getUser(), cameraConfig.getPassword());
968 Ffmpeg localRecord = ffmpegRecord;
969 if (localRecord != null) {
970 localRecord.startConverting();
971 if (mp4History.isEmpty()) {
972 mp4History = mp4Filename;
973 } else if (!mp4Filename.equals("ipcamera")) {
974 mp4History = mp4Filename + "," + mp4History;
975 if (mp4HistoryLength > 49) {
976 int endIndex = mp4History.lastIndexOf(",");
977 mp4History = mp4History.substring(0, endIndex);
981 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
984 Ffmpeg localAlarms = ffmpegRtspHelper;
985 if (localAlarms != null) {
986 localAlarms.stopConverting();
987 if (!audioAlarmEnabled && !motionAlarmEnabled) {
991 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
992 String filterOptions = "";
993 if (!audioAlarmEnabled) {
994 filterOptions = "-an";
996 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
998 if (!motionAlarmEnabled && !ffmpegSnapshotGeneration) {
999 filterOptions = filterOptions.concat(" -vn");
1000 } else if (motionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
1001 String usersMotionOptions = cameraConfig.getMotionOptions();
1002 if (usersMotionOptions.startsWith("-")) {
1003 // Need to put the users custom options first in the chain before the motion is detected
1004 filterOptions += " " + usersMotionOptions + ",select='gte(scene," + motionThreshold
1005 + ")',metadata=print";
1007 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
1008 + motionThreshold + ")',metadata=print";
1010 } else if (motionAlarmEnabled) {
1011 filterOptions = filterOptions
1012 .concat(" -vf select='gte(scene," + motionThreshold + ")',metadata=print");
1014 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
1015 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
1016 localAlarms = ffmpegRtspHelper;
1017 if (localAlarms != null) {
1018 localAlarms.startConverting();
1022 if (ffmpegMjpeg == null) {
1023 if (inputOptions.isEmpty()) {
1024 inputOptions = "-hide_banner -loglevel warning";
1026 inputOptions += " -hide_banner -loglevel warning";
1028 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1029 cameraConfig.getMjpegOptions(),
1030 "http://127.0.0.1:" + cameraConfig.getServerPort() + "/ipcamera.jpg",
1031 cameraConfig.getUser(), cameraConfig.getPassword());
1033 Ffmpeg localMjpeg = ffmpegMjpeg;
1034 if (localMjpeg != null) {
1035 localMjpeg.startConverting();
1039 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
1040 if (ffmpegSnapshot == null) {
1041 if (inputOptions.isEmpty()) {
1043 inputOptions = "-threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1045 inputOptions += " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1047 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1048 cameraConfig.getSnapshotOptions(),
1049 "http://127.0.0.1:" + cameraConfig.getServerPort() + "/snapshot.jpg",
1050 cameraConfig.getUser(), cameraConfig.getPassword());
1052 Ffmpeg localSnaps = ffmpegSnapshot;
1053 if (localSnaps != null) {
1054 localSnaps.startConverting();
1060 public void noMotionDetected(String thisAlarmsChannel) {
1061 setChannelState(thisAlarmsChannel, OnOffType.OFF);
1062 firstMotionAlarm = false;
1063 motionAlarmUpdateSnapshot = false;
1064 motionDetected = false;
1065 if (streamingAutoFps) {
1066 stopSnapshotPolling();
1067 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1068 stopSnapshotPolling();
1073 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
1074 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
1075 * tampering with the camera.
1077 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
1078 updateState(thisAlarmsChannel, state);
1081 public void motionDetected(String thisAlarmsChannel) {
1082 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
1083 updateState(thisAlarmsChannel, OnOffType.ON);
1084 motionDetected = true;
1085 if (streamingAutoFps) {
1086 startSnapshotPolling();
1088 if (cameraConfig.getUpdateImageWhen().contains("2")) {
1089 if (!firstMotionAlarm) {
1090 if (!snapshotUri.isEmpty()) {
1091 sendHttpGET(snapshotUri);
1093 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1095 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1096 if (!snapshotPolling) {
1097 startSnapshotPolling();
1099 firstMotionAlarm = true;
1100 motionAlarmUpdateSnapshot = true;
1104 public void audioDetected() {
1105 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1106 if (cameraConfig.getUpdateImageWhen().contains("3")) {
1107 if (!firstAudioAlarm) {
1108 if (!snapshotUri.isEmpty()) {
1109 sendHttpGET(snapshotUri);
1111 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1113 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1114 firstAudioAlarm = true;
1115 audioAlarmUpdateSnapshot = true;
1119 public void noAudioDetected() {
1120 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1121 firstAudioAlarm = false;
1122 audioAlarmUpdateSnapshot = false;
1125 public void recordMp4(String filename, int seconds) {
1126 mp4Filename = filename;
1127 mp4RecordTime = seconds;
1128 setupFfmpegFormat(FFmpegFormat.RECORD);
1129 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1132 public void recordGif(String filename, int seconds) {
1133 gifFilename = filename;
1134 gifRecordTime = seconds;
1135 if (cameraConfig.getGifPreroll() > 0) {
1136 snapCount = seconds;
1138 setupFfmpegFormat(FFmpegFormat.GIF);
1140 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1143 public String returnValueFromString(String rawString, String searchedString) {
1145 int index = rawString.indexOf(searchedString);
1146 if (index != -1) // -1 means "not found"
1148 result = rawString.substring(index + searchedString.length(), rawString.length());
1149 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1151 return result; // Did not find a carriage return.
1153 return result.substring(0, index);
1156 return ""; // Did not find the String we were searching for
1159 private void sendPTZRequest() {
1160 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1164 public void handleCommand(ChannelUID channelUID, Command command) {
1165 if (command instanceof RefreshType) {
1166 switch (channelUID.getId()) {
1168 if (onvifCamera.supportsPTZ()) {
1169 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1173 if (onvifCamera.supportsPTZ()) {
1174 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1178 if (onvifCamera.supportsPTZ()) {
1179 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1182 case CHANNEL_GOTO_PRESET:
1183 if (onvifCamera.supportsPTZ()) {
1184 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1188 } // caution "REFRESH" can still progress to brand Handlers below the else.
1190 switch (channelUID.getId()) {
1191 case CHANNEL_MP4_HISTORY_LENGTH:
1192 if (DecimalType.ZERO.equals(command)) {
1193 mp4HistoryLength = 0;
1195 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1198 case CHANNEL_GIF_HISTORY_LENGTH:
1199 if (DecimalType.ZERO.equals(command)) {
1200 gifHistoryLength = 0;
1202 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1205 case CHANNEL_FFMPEG_MOTION_CONTROL:
1206 if (OnOffType.ON.equals(command)) {
1207 motionAlarmEnabled = true;
1208 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1209 motionAlarmEnabled = false;
1210 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1212 motionAlarmEnabled = true;
1213 motionThreshold = Double.valueOf(command.toString());
1214 motionThreshold = motionThreshold / 10000;
1216 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1218 case CHANNEL_START_STREAM:
1220 if (OnOffType.ON.equals(command)) {
1221 localHLS = ffmpegHLS;
1222 if (localHLS == null) {
1223 setupFfmpegFormat(FFmpegFormat.HLS);
1224 localHLS = ffmpegHLS;
1226 if (localHLS != null) {
1227 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1228 localHLS.startConverting();
1231 localHLS = ffmpegHLS;
1232 if (localHLS != null) {
1233 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1234 localHLS.setKeepAlive(1);
1238 case CHANNEL_EXTERNAL_MOTION:
1239 if (OnOffType.ON.equals(command)) {
1240 motionDetected(CHANNEL_EXTERNAL_MOTION);
1242 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1245 case CHANNEL_GOTO_PRESET:
1246 if (onvifCamera.supportsPTZ()) {
1247 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1250 case CHANNEL_POLL_IMAGE:
1251 if (OnOffType.ON.equals(command)) {
1252 if (snapshotUri.isEmpty()) {
1253 ffmpegSnapshotGeneration = true;
1254 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1255 updateImageChannel = false;
1257 updateImageChannel = true;
1258 sendHttpGET(snapshotUri);// Allows this to change Image FPS on demand
1261 Ffmpeg localSnaps = ffmpegSnapshot;
1262 if (localSnaps != null) {
1263 localSnaps.stopConverting();
1264 ffmpegSnapshotGeneration = false;
1266 updateImageChannel = false;
1270 if (onvifCamera.supportsPTZ()) {
1271 if (command instanceof IncreaseDecreaseType) {
1272 if (command == IncreaseDecreaseType.INCREASE) {
1273 if (cameraConfig.getPtzContinuous()) {
1274 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1276 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1279 if (cameraConfig.getPtzContinuous()) {
1280 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1282 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1286 } else if (OnOffType.OFF.equals(command)) {
1287 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1290 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1291 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1295 if (onvifCamera.supportsPTZ()) {
1296 if (command instanceof IncreaseDecreaseType) {
1297 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1298 if (cameraConfig.getPtzContinuous()) {
1299 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1301 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1304 if (cameraConfig.getPtzContinuous()) {
1305 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1307 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1311 } else if (OnOffType.OFF.equals(command)) {
1312 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1315 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1316 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1320 if (onvifCamera.supportsPTZ()) {
1321 if (command instanceof IncreaseDecreaseType) {
1322 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1323 if (cameraConfig.getPtzContinuous()) {
1324 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1326 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1329 if (cameraConfig.getPtzContinuous()) {
1330 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1332 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1336 } else if (OnOffType.OFF.equals(command)) {
1337 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1340 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1341 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1346 // commands and refresh now get passed to brand handlers
1347 switch (thing.getThingTypeUID().getId()) {
1349 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1350 amcrestHandler.handleCommand(channelUID, command);
1351 if (lowPriorityRequests.isEmpty()) {
1352 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1356 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1357 dahuaHandler.handleCommand(channelUID, command);
1358 if (lowPriorityRequests.isEmpty()) {
1359 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1362 case DOORBIRD_THING:
1363 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1364 doorBirdHandler.handleCommand(channelUID, command);
1365 if (lowPriorityRequests.isEmpty()) {
1366 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1369 case HIKVISION_THING:
1370 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1371 hikvisionHandler.handleCommand(channelUID, command);
1372 if (lowPriorityRequests.isEmpty()) {
1373 lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1377 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1378 cameraConfig.getPassword());
1379 foscamHandler.handleCommand(channelUID, command);
1380 if (lowPriorityRequests.isEmpty()) {
1381 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1385 InstarHandler instarHandler = new InstarHandler(getHandle());
1386 instarHandler.handleCommand(channelUID, command);
1387 if (lowPriorityRequests.isEmpty()) {
1388 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1392 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1393 defaultHandler.handleCommand(channelUID, command);
1394 if (lowPriorityRequests.isEmpty()) {
1395 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1401 public void setChannelState(String channelToUpdate, State valueOf) {
1402 updateState(channelToUpdate, valueOf);
1405 void bringCameraOnline() {
1407 updateStatus(ThingStatus.ONLINE);
1408 groupTracker.listOfOnlineCameraHandlers.add(this);
1409 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1410 Future<?> localFuture = cameraConnectionJob;
1411 if (localFuture != null) {
1412 localFuture.cancel(false);
1415 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1416 snapshotPolling = true;
1417 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 1000, cameraConfig.getPollTime(),
1418 TimeUnit.MILLISECONDS);
1421 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1423 if (!rtspUri.isEmpty()) {
1424 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1426 if (updateImageChannel) {
1427 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1429 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1431 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1432 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1433 handle.cameraOnline(getThing().getUID().getId());
1438 void snapshotIsFfmpeg() {
1439 bringCameraOnline();
1440 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1442 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1443 if (!rtspUri.isEmpty()) {
1444 updateImageChannel = false;
1445 ffmpegSnapshotGeneration = true;
1446 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1447 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1449 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1453 void pollingCameraConnection() {
1454 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1455 if (rtspUri.isEmpty()) {
1456 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1458 if (snapshotUri.isEmpty() || snapshotUri.equals("ffmpeg")) {
1461 sendHttpRequest("GET", snapshotUri, null);
1465 if (!onvifCamera.isConnected()) {
1466 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1467 cameraConfig.getOnvifPort());
1468 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1470 if (snapshotUri.equals("ffmpeg")) {
1472 } else if (!snapshotUri.isEmpty()) {
1473 sendHttpRequest("GET", snapshotUri, null);
1474 } else if (!rtspUri.isEmpty()) {
1477 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1478 "Camera failed to report a valid Snaphot and/or RTSP URL. See readme on how to use the SNAPSHOT_URL_OVERRIDE feature.");
1482 public void cameraConfigError(String reason) {
1483 // wont try to reconnect again due to a config error being the cause.
1484 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1488 public void cameraCommunicationError(String reason) {
1489 // will try to reconnect again as camera may be rebooting.
1490 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1491 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1492 resetAndRetryConnecting();
1496 boolean streamIsStopped(String url) {
1497 ChannelTracking channelTracking = channelTrackingMap.get(url);
1498 if (channelTracking != null) {
1499 if (channelTracking.getChannel().isActive()) {
1500 return false; // stream is running.
1503 return true; // Stream stopped or never started.
1506 void snapshotRunnable() {
1507 // Snapshot should be first to keep consistent time between shots
1508 sendHttpGET(snapshotUri);
1509 if (snapCount > 0) {
1510 if (--snapCount == 0) {
1511 setupFfmpegFormat(FFmpegFormat.GIF);
1516 public void stopSnapshotPolling() {
1517 Future<?> localFuture;
1518 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1519 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1520 snapshotPolling = false;
1521 localFuture = snapshotJob;
1522 if (localFuture != null) {
1523 localFuture.cancel(true);
1525 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1526 snapshotPolling = false;
1527 localFuture = snapshotJob;
1528 if (localFuture != null) {
1529 localFuture.cancel(true);
1534 public void startSnapshotPolling() {
1535 if (snapshotPolling || ffmpegSnapshotGeneration) {
1536 return; // Already polling or creating with FFmpeg from RTSP
1538 if (streamingSnapshotMjpeg || streamingAutoFps) {
1539 snapshotPolling = true;
1540 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1541 TimeUnit.MILLISECONDS);
1542 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1543 snapshotPolling = true;
1544 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1545 TimeUnit.MILLISECONDS);
1550 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep mjpeg and alarm
1551 * streams open and more.
1554 void pollCameraRunnable() {
1555 // Snapshot should be first to keep consistent time between shots
1556 if (streamingAutoFps) {
1557 updateAutoFps = true;
1558 if (!snapshotPolling && !ffmpegSnapshotGeneration) {
1559 // Dont need to poll if creating from RTSP stream with FFmpeg or we are polling at full rate already.
1560 sendHttpGET(snapshotUri);
1562 } else if (!snapshotUri.isEmpty() && !snapshotPolling) {// we need to check camera is still online.
1563 sendHttpGET(snapshotUri);
1565 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1566 if (!lowPriorityRequests.isEmpty()) {
1567 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1568 lowPriorityCounter = 0;
1570 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1572 // what needs to be done every poll//
1573 switch (thing.getThingTypeUID().getId()) {
1577 if (!onvifCamera.isConnected()) {
1578 onvifCamera.connect(true);
1582 noMotionDetected(CHANNEL_MOTION_ALARM);
1583 noMotionDetected(CHANNEL_PIR_ALARM);
1586 case HIKVISION_THING:
1587 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1588 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1589 cameraConfig.getIp());
1590 sendHttpGET("/ISAPI/Event/notification/alertStream");
1594 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1595 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1598 // Check for alarms, channel for NVRs appears not to work at filtering.
1599 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1600 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1601 cameraConfig.getIp());
1602 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1605 case DOORBIRD_THING:
1606 // Check for alarms, channel for NVRs appears not to work at filtering.
1607 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1608 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1609 cameraConfig.getIp());
1610 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1614 Ffmpeg localHLS = ffmpegHLS;
1615 if (localHLS != null) {
1616 localHLS.checkKeepAlive();
1618 if (openChannels.size() > 18) {
1619 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1625 public void initialize() {
1626 cameraConfig = getConfigAs(CameraConfig.class);
1627 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1628 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1629 rtspUri = cameraConfig.getFfmpegInput();
1631 if (cameraConfig.getServerPort() < 1) {
1633 "The Server Port is not set to a valid number which disables a lot of binding features. See readme for more info.");
1634 } else if (cameraConfig.getServerPort() < 1025) {
1635 logger.warn("The Server Port is <= 1024 and may cause permission errors under Linux, try a higher number.");
1638 // Known cameras will connect quicker if we skip ONVIF questions.
1639 switch (thing.getThingTypeUID().getId()) {
1642 if (mjpegUri.isEmpty()) {
1643 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1645 if (snapshotUri.isEmpty()) {
1646 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1649 case DOORBIRD_THING:
1650 if (mjpegUri.isEmpty()) {
1651 mjpegUri = "/bha-api/video.cgi";
1653 if (snapshotUri.isEmpty()) {
1654 snapshotUri = "/bha-api/image.cgi";
1658 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1659 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1660 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1661 if (mjpegUri.isEmpty()) {
1662 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1663 + cameraConfig.getPassword();
1665 if (snapshotUri.isEmpty()) {
1666 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1667 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1670 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1671 if (mjpegUri.isEmpty()) {
1672 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1674 if (snapshotUri.isEmpty()) {
1675 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1679 if (snapshotUri.isEmpty()) {
1680 snapshotUri = "/tmpfs/snap.jpg";
1682 if (mjpegUri.isEmpty()) {
1683 mjpegUri = "/mjpegstream.cgi?-chn=12";
1688 // Onvif and Instar event handling needs the host IP and the server started.
1689 if (cameraConfig.getServerPort() > 0) {
1690 startStreamServer();
1693 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1694 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1695 cameraConfig.getUser(), cameraConfig.getPassword());
1696 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1697 // Only use ONVIF events if it is not an API camera.
1698 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1701 // for poll times above 9 seconds don't display a warning about the Image channel.
1702 if (9000 <= cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1704 "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.");
1706 // Waiting 3 seconds for ONVIF to discover the urls before running.
1707 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 30, TimeUnit.SECONDS);
1710 // What the camera needs to re-connect if the initialize() is not called.
1711 private void resetAndRetryConnecting() {
1717 public void dispose() {
1719 snapshotPolling = false;
1720 onvifCamera.disconnect();
1721 Future<?> localFuture = pollCameraJob;
1722 if (localFuture != null) {
1723 localFuture.cancel(true);
1725 localFuture = snapshotJob;
1726 if (localFuture != null) {
1727 localFuture.cancel(true);
1729 localFuture = cameraConnectionJob;
1730 if (localFuture != null) {
1731 localFuture.cancel(true);
1733 threadPool.shutdown();
1734 threadPool = Executors.newScheduledThreadPool(4);
1736 groupTracker.listOfOnlineCameraHandlers.remove(this);
1737 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1738 // inform all group handlers that this camera has gone offline
1739 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1740 handle.cameraOffline(this);
1742 basicAuth = ""; // clear out stored Password hash
1743 useDigestAuth = false;
1745 openChannels.close();
1747 Ffmpeg localFfmpeg = ffmpegHLS;
1748 if (localFfmpeg != null) {
1749 localFfmpeg.stopConverting();
1751 localFfmpeg = ffmpegRecord;
1752 if (localFfmpeg != null) {
1753 localFfmpeg.stopConverting();
1755 localFfmpeg = ffmpegGIF;
1756 if (localFfmpeg != null) {
1757 localFfmpeg.stopConverting();
1759 localFfmpeg = ffmpegRtspHelper;
1760 if (localFfmpeg != null) {
1761 localFfmpeg.stopConverting();
1763 localFfmpeg = ffmpegMjpeg;
1764 if (localFfmpeg != null) {
1765 localFfmpeg.stopConverting();
1767 localFfmpeg = ffmpegSnapshot;
1768 if (localFfmpeg != null) {
1769 localFfmpeg.stopConverting();
1771 channelTrackingMap.clear();
1774 public void setStreamServerHandler(StreamServerHandler streamServerHandler2) {
1775 streamServerHandler = streamServerHandler2;
1778 public String getWhiteList() {
1779 return cameraConfig.getIpWhitelist();
1783 public Collection<Class<? extends ThingHandlerService>> getServices() {
1784 return Collections.singleton(IpCameraActions.class);