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.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.MyNettyAuthHandler;
57 import org.openhab.binding.ipcamera.internal.StreamServerHandler;
58 import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection;
59 import org.openhab.core.library.types.DecimalType;
60 import org.openhab.core.library.types.IncreaseDecreaseType;
61 import org.openhab.core.library.types.OnOffType;
62 import org.openhab.core.library.types.PercentType;
63 import org.openhab.core.library.types.RawType;
64 import org.openhab.core.library.types.StringType;
65 import org.openhab.core.thing.ChannelUID;
66 import org.openhab.core.thing.Thing;
67 import org.openhab.core.thing.ThingStatus;
68 import org.openhab.core.thing.ThingStatusDetail;
69 import org.openhab.core.thing.binding.BaseThingHandler;
70 import org.openhab.core.thing.binding.ThingHandlerService;
71 import org.openhab.core.types.Command;
72 import org.openhab.core.types.RefreshType;
73 import org.openhab.core.types.State;
74 import org.slf4j.Logger;
75 import org.slf4j.LoggerFactory;
77 import io.netty.bootstrap.Bootstrap;
78 import io.netty.bootstrap.ServerBootstrap;
79 import io.netty.buffer.ByteBuf;
80 import io.netty.buffer.Unpooled;
81 import io.netty.channel.Channel;
82 import io.netty.channel.ChannelDuplexHandler;
83 import io.netty.channel.ChannelFuture;
84 import io.netty.channel.ChannelFutureListener;
85 import io.netty.channel.ChannelHandlerContext;
86 import io.netty.channel.ChannelInitializer;
87 import io.netty.channel.ChannelOption;
88 import io.netty.channel.EventLoopGroup;
89 import io.netty.channel.group.ChannelGroup;
90 import io.netty.channel.group.DefaultChannelGroup;
91 import io.netty.channel.nio.NioEventLoopGroup;
92 import io.netty.channel.socket.SocketChannel;
93 import io.netty.channel.socket.nio.NioServerSocketChannel;
94 import io.netty.channel.socket.nio.NioSocketChannel;
95 import io.netty.handler.codec.base64.Base64;
96 import io.netty.handler.codec.http.DefaultFullHttpRequest;
97 import io.netty.handler.codec.http.DefaultHttpResponse;
98 import io.netty.handler.codec.http.FullHttpRequest;
99 import io.netty.handler.codec.http.HttpClientCodec;
100 import io.netty.handler.codec.http.HttpContent;
101 import io.netty.handler.codec.http.HttpHeaderNames;
102 import io.netty.handler.codec.http.HttpHeaderValues;
103 import io.netty.handler.codec.http.HttpMessage;
104 import io.netty.handler.codec.http.HttpMethod;
105 import io.netty.handler.codec.http.HttpResponse;
106 import io.netty.handler.codec.http.HttpResponseStatus;
107 import io.netty.handler.codec.http.HttpServerCodec;
108 import io.netty.handler.codec.http.HttpVersion;
109 import io.netty.handler.codec.http.LastHttpContent;
110 import io.netty.handler.stream.ChunkedWriteHandler;
111 import io.netty.handler.timeout.IdleState;
112 import io.netty.handler.timeout.IdleStateEvent;
113 import io.netty.handler.timeout.IdleStateHandler;
114 import io.netty.util.CharsetUtil;
115 import io.netty.util.ReferenceCountUtil;
116 import io.netty.util.concurrent.GlobalEventExecutor;
119 * The {@link IpCameraHandler} is responsible for handling commands, which are
120 * sent to one of the channels.
122 * @author Matthew Skinner - Initial contribution
126 public class IpCameraHandler extends BaseThingHandler {
127 public final Logger logger = LoggerFactory.getLogger(getClass());
128 private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(4);
129 private GroupTracker groupTracker;
130 public CameraConfig cameraConfig;
132 // ChannelGroup is thread safe
133 public final ChannelGroup mjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
134 private final ChannelGroup snapshotMjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
135 private final ChannelGroup autoSnapshotMjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
136 public final ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
137 public @Nullable Ffmpeg ffmpegHLS = null;
138 public @Nullable Ffmpeg ffmpegRecord = null;
139 public @Nullable Ffmpeg ffmpegGIF = null;
140 public @Nullable Ffmpeg ffmpegRtspHelper = null;
141 public @Nullable Ffmpeg ffmpegMjpeg = null;
142 public @Nullable Ffmpeg ffmpegSnapshot = null;
143 public boolean streamingAutoFps = false;
144 public boolean motionDetected = false;
146 private @Nullable ScheduledFuture<?> cameraConnectionJob = null;
147 private @Nullable ScheduledFuture<?> pollCameraJob = null;
148 private @Nullable ScheduledFuture<?> snapshotJob = null;
149 private @Nullable Bootstrap mainBootstrap;
150 private @Nullable ServerBootstrap serverBootstrap;
152 private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup();
153 private EventLoopGroup serversLoopGroup = new NioEventLoopGroup();
154 private FullHttpRequest putRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
156 private String gifFilename = "ipcamera";
157 private String gifHistory = "";
158 private String mp4History = "";
159 public int gifHistoryLength;
160 public int mp4HistoryLength;
161 private String mp4Filename = "ipcamera";
162 private int mp4RecordTime;
163 private int gifRecordTime = 5;
164 private int mp4Preroll;
165 private LinkedList<byte[]> fifoSnapshotBuffer = new LinkedList<byte[]>();
166 private int snapCount;
167 private boolean updateImageChannel = false;
168 private boolean updateAutoFps = false;
169 private byte lowPriorityCounter = 0;
170 public String hostIp;
171 public Map<String, ChannelTracking> channelTrackingMap = new ConcurrentHashMap<>();
172 public List<String> lowPriorityRequests = new ArrayList<>(0);
174 // basicAuth MUST remain private as it holds the cameraConfig.getPassword()
175 private String basicAuth = "";
176 public boolean useBasicAuth = false;
177 public boolean useDigestAuth = false;
178 public String snapshotUri = "";
179 public String mjpegUri = "";
180 private @Nullable ChannelFuture serverFuture = null;
181 private Object firstStreamedMsg = new Object();
182 public byte[] currentSnapshot = new byte[] { (byte) 0x00 };
183 public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
184 public String rtspUri = "";
185 public boolean audioAlarmUpdateSnapshot = false;
186 private boolean motionAlarmUpdateSnapshot = false;
187 private boolean isOnline = false; // Used so only 1 error is logged when a network issue occurs.
188 private boolean firstAudioAlarm = false;
189 private boolean firstMotionAlarm = false;
190 public Double motionThreshold = 0.0016;
191 public int audioThreshold = 35;
192 @SuppressWarnings("unused")
193 private @Nullable StreamServerHandler streamServerHandler;
194 private boolean streamingSnapshotMjpeg = false;
195 public boolean motionAlarmEnabled = false;
196 public boolean audioAlarmEnabled = false;
197 public boolean ffmpegSnapshotGeneration = false;
198 public boolean snapshotPolling = false;
199 public OnvifConnection onvifCamera = new OnvifConnection(this, "", "", "");
201 // These methods handle the response from all camera brands, nothing specific to 1 brand.
202 private class CommonCameraHandler extends ChannelDuplexHandler {
203 private int bytesToRecieve = 0;
204 private int bytesAlreadyRecieved = 0;
205 private byte[] incomingJpeg = new byte[0];
206 private String incomingMessage = "";
207 private String contentType = "empty";
208 private Object reply = new Object();
209 private String requestUrl = "";
210 private boolean closeConnection = true;
211 private boolean isChunked = false;
213 public void setURL(String url) {
218 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
219 if (msg == null || ctx == null) {
223 if (msg instanceof HttpResponse) {
224 HttpResponse response = (HttpResponse) msg;
225 if (response.status().code() != 401) {
226 if (!response.headers().isEmpty()) {
227 for (String name : response.headers().names()) {
228 // Some cameras use first letter uppercase and others dont.
229 switch (name.toLowerCase()) { // Possible localization issues doing this
231 contentType = response.headers().getAsString(name);
233 case "content-length":
234 bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
237 if (response.headers().getAsString(name).contains("keep-alive")) {
238 closeConnection = false;
241 case "transfer-encoding":
242 if (response.headers().getAsString(name).contains("chunked")) {
248 if (contentType.contains("multipart")) {
249 closeConnection = false;
250 if (mjpegUri.contains(requestUrl)) {
251 if (msg instanceof HttpMessage) {
252 // very start of stream only
253 ReferenceCountUtil.retain(msg, 1);
254 firstStreamedMsg = msg;
255 streamToGroup(firstStreamedMsg, mjpegChannelGroup, true);
258 } else if (contentType.contains("image/jp")) {
259 if (bytesToRecieve == 0) {
260 bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
261 logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
263 incomingJpeg = new byte[bytesToRecieve];
268 if (msg instanceof HttpContent) {
269 if (mjpegUri.contains(requestUrl)) {
270 // multiple MJPEG stream packets come back as this.
271 ReferenceCountUtil.retain(msg, 1);
272 streamToGroup(msg, mjpegChannelGroup, true);
274 HttpContent content = (HttpContent) msg;
275 // Found some cameras uses Content-Type: image/jpg instead of image/jpeg
276 if (contentType.contains("image/jp")) {
277 for (int i = 0; i < content.content().capacity(); i++) {
278 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
280 if (content instanceof LastHttpContent) {
281 processSnapshot(incomingJpeg);
282 // testing next line and if works need to do a full cleanup of this function.
283 closeConnection = true;
284 if (closeConnection) {
288 bytesAlreadyRecieved = 0;
291 } else { // incomingMessage that is not an IMAGE
292 if (incomingMessage.isEmpty()) {
293 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
295 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
297 bytesAlreadyRecieved = incomingMessage.length();
298 if (content instanceof LastHttpContent) {
299 // If it is not an image send it on to the next handler//
300 if (bytesAlreadyRecieved != 0) {
301 reply = incomingMessage;
302 super.channelRead(ctx, reply);
305 // HIKVISION alertStream never has a LastHttpContent as it always stays open//
306 if (contentType.contains("multipart")) {
307 if (bytesAlreadyRecieved != 0) {
308 reply = incomingMessage;
309 incomingMessage = "";
311 bytesAlreadyRecieved = 0;
312 super.channelRead(ctx, reply);
315 // Foscam needs this as will other cameras with chunks//
316 if (isChunked && bytesAlreadyRecieved != 0) {
317 reply = incomingMessage;
318 super.channelRead(ctx, reply);
322 } else { // msg is not HttpContent
323 // Foscam and Amcrest cameras need this
324 if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
325 reply = incomingMessage;
326 logger.debug("Packet back from camera is {}", incomingMessage);
327 super.channelRead(ctx, reply);
331 ReferenceCountUtil.release(msg);
336 public void channelReadComplete(@Nullable ChannelHandlerContext ctx) {
340 public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
344 public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
348 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
349 if (cause == null || ctx == null) {
352 if (cause instanceof ArrayIndexOutOfBoundsException) {
353 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
356 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
363 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
367 if (evt instanceof IdleStateEvent) {
368 IdleStateEvent e = (IdleStateEvent) evt;
369 // If camera does not use the channel for X amount of time it will close.
370 if (e.state() == IdleState.READER_IDLE) {
371 String urlToKeepOpen = "";
372 switch (thing.getThingTypeUID().getId()) {
374 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
376 case HIKVISION_THING:
377 urlToKeepOpen = "/ISAPI/Event/notification/alertStream";
380 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
383 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
384 if (channelTracking != null) {
385 if (channelTracking.getChannel() == ctx.channel()) {
386 return; // don't auto close this as it is for the alarms.
395 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker) {
397 cameraConfig = getConfigAs(CameraConfig.class);
398 if (ipAddress != null) {
401 hostIp = Helper.getLocalIpAddress();
403 this.groupTracker = groupTracker;
406 private IpCameraHandler getHandle() {
410 // false clears the stored user/pass hash, true creates the hash
411 public boolean setBasicAuth(boolean useBasic) {
412 if (useBasic == false) {
413 logger.debug("Clearing out the stored BASIC auth now.");
416 } else if (!basicAuth.isEmpty()) {
417 // due to camera may have been sent multiple requests before the auth was set, this may trigger falsely.
418 logger.warn("Camera is reporting your username and/or password is wrong.");
421 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
422 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
423 ByteBuf byteBuf = null;
425 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
426 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
428 if (byteBuf != null) {
434 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
439 private String getCorrectUrlFormat(String longUrl) {
440 String temp = longUrl;
443 if (longUrl.isEmpty() || longUrl.equals("ffmpeg")) {
448 url = new URL(longUrl);
449 int port = url.getPort();
451 if (url.getQuery() == null) {
452 temp = url.getPath();
454 temp = url.getPath() + "?" + url.getQuery();
457 if (url.getQuery() == null) {
458 temp = ":" + url.getPort() + url.getPath();
460 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
463 } catch (MalformedURLException e) {
464 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
469 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
470 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
471 sendHttpRequest("PUT", httpRequestURL, null);
474 public void sendHttpGET(String httpRequestURL) {
475 sendHttpRequest("GET", httpRequestURL, null);
478 public int getPortFromShortenedUrl(String httpRequestURL) {
479 if (httpRequestURL.startsWith(":")) {
480 int end = httpRequestURL.indexOf("/");
481 return Integer.parseInt(httpRequestURL.substring(1, end));
483 return cameraConfig.getPort();
486 public String getTinyUrl(String httpRequestURL) {
487 if (httpRequestURL.startsWith(":")) {
488 int beginIndex = httpRequestURL.indexOf("/");
489 return httpRequestURL.substring(beginIndex);
491 return httpRequestURL;
494 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
495 // The authHandler will generate a digest string and re-send using this same function when needed.
496 @SuppressWarnings("null")
497 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
499 int port = getPortFromShortenedUrl(httpRequestURLFull);
500 String httpRequestURL = getTinyUrl(httpRequestURLFull);
502 if (mainBootstrap == null) {
503 mainBootstrap = new Bootstrap();
504 mainBootstrap.group(mainEventLoopGroup);
505 mainBootstrap.channel(NioSocketChannel.class);
506 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
507 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
508 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
509 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
510 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
511 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
514 public void initChannel(SocketChannel socketChannel) throws Exception {
515 // HIK Alarm stream needs > 9sec idle to stop stream closing
516 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
517 socketChannel.pipeline().addLast(new HttpClientCodec());
518 socketChannel.pipeline().addLast(AUTH_HANDLER,
519 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
520 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
522 switch (thing.getThingTypeUID().getId()) {
524 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
527 socketChannel.pipeline()
528 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
531 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
534 socketChannel.pipeline().addLast(
535 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
537 case HIKVISION_THING:
538 socketChannel.pipeline()
539 .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
542 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
545 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
552 FullHttpRequest request;
553 if (!"PUT".equals(httpMethod) || (useDigestAuth && digestString == null)) {
554 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
555 request.headers().set(HttpHeaderNames.HOST, cameraConfig.getIp());
556 request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
558 request = putRequestWithBody;
561 if (!basicAuth.isEmpty()) {
563 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
566 request.headers().set(HttpHeaderNames.AUTHORIZATION, "Basic " + basicAuth);
571 if (digestString != null) {
572 request.headers().set(HttpHeaderNames.AUTHORIZATION, "Digest " + digestString);
576 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
577 .addListener(new ChannelFutureListener() {
580 public void operationComplete(@Nullable ChannelFuture future) {
581 if (future == null) {
584 if (future.isDone() && future.isSuccess()) {
585 Channel ch = future.channel();
586 openChannels.add(ch);
590 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
592 channelTrackingMap.put(httpRequestURL, new ChannelTracking(ch, httpRequestURL));
594 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
595 commonHandler.setURL(httpRequestURLFull);
596 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
597 authHandler.setURL(httpMethod, httpRequestURL);
599 switch (thing.getThingTypeUID().getId()) {
601 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
602 amcrestHandler.setURL(httpRequestURL);
605 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
606 instarHandler.setURL(httpRequestURL);
609 ch.writeAndFlush(request);
610 } else { // an error occured
611 cameraCommunicationError(
612 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
618 public void processSnapshot(byte[] incommingSnapshot) {
619 lockCurrentSnapshot.lock();
621 currentSnapshot = incommingSnapshot;
622 if (cameraConfig.getGifPreroll() > 0) {
623 fifoSnapshotBuffer.add(incommingSnapshot);
624 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
625 fifoSnapshotBuffer.removeFirst();
629 lockCurrentSnapshot.unlock();
632 if (streamingSnapshotMjpeg) {
633 sendMjpegFrame(incommingSnapshot, snapshotMjpegChannelGroup);
635 if (streamingAutoFps) {
636 if (motionDetected) {
637 sendMjpegFrame(incommingSnapshot, autoSnapshotMjpegChannelGroup);
638 } else if (updateAutoFps) {
639 // only happens every 8 seconds as some browsers need a frame that often to keep stream alive.
640 sendMjpegFrame(incommingSnapshot, autoSnapshotMjpegChannelGroup);
641 updateAutoFps = false;
645 if (updateImageChannel) {
646 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
647 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
648 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
649 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
650 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
651 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
652 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
656 public void stopStreamServer() {
657 serversLoopGroup.shutdownGracefully();
658 serverBootstrap = null;
661 @SuppressWarnings("null")
662 public void startStreamServer() {
663 if (serverBootstrap == null) {
665 serversLoopGroup = new NioEventLoopGroup();
666 serverBootstrap = new ServerBootstrap();
667 serverBootstrap.group(serversLoopGroup);
668 serverBootstrap.channel(NioServerSocketChannel.class);
669 // IP "0.0.0.0" will bind the server to all network connections//
670 serverBootstrap.localAddress(new InetSocketAddress("0.0.0.0", cameraConfig.getServerPort()));
671 serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
673 protected void initChannel(SocketChannel socketChannel) throws Exception {
674 socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 60, 0));
675 socketChannel.pipeline().addLast("HttpServerCodec", new HttpServerCodec());
676 socketChannel.pipeline().addLast("ChunkedWriteHandler", new ChunkedWriteHandler());
677 socketChannel.pipeline().addLast("streamServerHandler", new StreamServerHandler(getHandle()));
680 serverFuture = serverBootstrap.bind().sync();
681 serverFuture.await(4000);
682 logger.debug("File server for camera at {} has started on port {} for all NIC's.", cameraConfig.getIp(),
683 cameraConfig.getServerPort());
684 updateState(CHANNEL_MJPEG_URL,
685 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.mjpeg"));
686 updateState(CHANNEL_HLS_URL,
687 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.m3u8"));
688 updateState(CHANNEL_IMAGE_URL,
689 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.jpg"));
690 } catch (Exception e) {
691 cameraConfigError("Exception when starting server. Try changing the Server Port to another number.");
696 public void setupSnapshotStreaming(boolean stream, ChannelHandlerContext ctx, boolean auto) {
698 sendMjpegFirstPacket(ctx);
700 autoSnapshotMjpegChannelGroup.add(ctx.channel());
701 lockCurrentSnapshot.lock();
703 sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
704 // iOS uses a FIFO? and needs two frames to display a pic
705 sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
707 lockCurrentSnapshot.unlock();
709 streamingAutoFps = true;
711 snapshotMjpegChannelGroup.add(ctx.channel());
712 lockCurrentSnapshot.lock();
714 sendMjpegFrame(currentSnapshot, snapshotMjpegChannelGroup);
716 lockCurrentSnapshot.unlock();
718 streamingSnapshotMjpeg = true;
719 startSnapshotPolling();
722 snapshotMjpegChannelGroup.remove(ctx.channel());
723 autoSnapshotMjpegChannelGroup.remove(ctx.channel());
724 if (streamingSnapshotMjpeg && snapshotMjpegChannelGroup.isEmpty()) {
725 streamingSnapshotMjpeg = false;
726 stopSnapshotPolling();
727 logger.debug("All snapshots.mjpeg streams have stopped.");
728 } else if (streamingAutoFps && autoSnapshotMjpegChannelGroup.isEmpty()) {
729 streamingAutoFps = false;
730 stopSnapshotPolling();
731 logger.debug("All autofps.mjpeg streams have stopped.");
736 // If start is true the CTX is added to the list to stream video to, false stops
738 public void setupMjpegStreaming(boolean start, ChannelHandlerContext ctx) {
740 if (mjpegChannelGroup.isEmpty()) {// first stream being requested.
741 mjpegChannelGroup.add(ctx.channel());
742 if (mjpegUri.isEmpty() || mjpegUri.equals("ffmpeg")) {
743 sendMjpegFirstPacket(ctx);
744 setupFfmpegFormat(FFmpegFormat.MJPEG);
747 // fix Dahua reboots when refreshing a mjpeg stream.
748 TimeUnit.MILLISECONDS.sleep(500);
749 } catch (InterruptedException e) {
751 sendHttpGET(mjpegUri);
753 } else if (ffmpegMjpeg != null) {// not first stream and we will use ffmpeg
754 sendMjpegFirstPacket(ctx);
755 mjpegChannelGroup.add(ctx.channel());
756 } else {// not first stream and camera supplies the mjpeg source.
757 ctx.channel().writeAndFlush(firstStreamedMsg);
758 mjpegChannelGroup.add(ctx.channel());
761 mjpegChannelGroup.remove(ctx.channel());
762 if (mjpegChannelGroup.isEmpty()) {
763 logger.debug("All ipcamera.mjpeg streams have stopped.");
764 if (mjpegUri.equals("ffmpeg")) {
765 if (ffmpegMjpeg != null) {
766 ffmpegMjpeg.stopConverting();
768 } else if (!mjpegUri.isEmpty()) {
769 closeChannel(getTinyUrl(mjpegUri));
771 if (ffmpegMjpeg != null) {
772 ffmpegMjpeg.stopConverting();
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";
893 inputOptions = inputOptions + " -rtsp_transport tcp";
897 // Make sure the folder exists, if not create it.
898 new File(cameraConfig.getFfmpegOutput()).mkdirs();
901 if (ffmpegHLS == null) {
902 if (!inputOptions.isEmpty()) {
903 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
904 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
905 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
906 cameraConfig.getUser(), cameraConfig.getPassword());
908 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
909 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
910 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
911 cameraConfig.getPassword());
914 if (ffmpegHLS != null) {
915 ffmpegHLS.startConverting();
919 if (cameraConfig.getGifPreroll() > 0) {
920 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
921 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
922 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
923 + cameraConfig.getGifOutOptions(),
924 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
925 cameraConfig.getPassword());
927 if (!inputOptions.isEmpty()) {
928 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
930 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
932 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
933 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
934 cameraConfig.getUser(), cameraConfig.getPassword());
936 if (cameraConfig.getGifPreroll() > 0) {
939 if (ffmpegGIF != null) {
940 ffmpegGIF.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 if (mp4Preroll > 0) {
963 // fetchFromHLS(); todo: not done yet
965 if (ffmpegRecord != null) {
966 ffmpegRecord.startConverting();
967 if (mp4History.isEmpty()) {
968 mp4History = mp4Filename;
969 } else if (!mp4Filename.equals("ipcamera")) {
970 mp4History = mp4Filename + "," + mp4History;
971 if (mp4HistoryLength > 49) {
972 int endIndex = mp4History.lastIndexOf(",");
973 mp4History = mp4History.substring(0, endIndex);
976 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
980 if (ffmpegRtspHelper != null) {
981 ffmpegRtspHelper.stopConverting();
982 if (!audioAlarmEnabled && !motionAlarmEnabled) {
986 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
987 String OutputOptions = "-f null -";
988 String filterOptions = "";
989 if (!audioAlarmEnabled) {
990 filterOptions = "-an";
992 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
994 if (!motionAlarmEnabled && !ffmpegSnapshotGeneration) {
995 filterOptions = filterOptions.concat(" -vn");
996 } else if (motionAlarmEnabled == true) {
997 filterOptions = filterOptions
998 .concat(" -vf select='gte(scene," + motionThreshold + ")',metadata=print");
1000 if (!cameraConfig.getUser().isEmpty()) {
1001 filterOptions += " ";// add space as the Framework does not allow spaces at start of config.
1003 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
1004 filterOptions + cameraConfig.getMotionOptions(), OutputOptions, cameraConfig.getUser(),
1005 cameraConfig.getPassword());
1006 ffmpegRtspHelper.startConverting();
1009 if (ffmpegMjpeg == null) {
1010 if (inputOptions.isEmpty()) {
1011 inputOptions = "-hide_banner -loglevel warning";
1013 inputOptions = inputOptions + " -hide_banner -loglevel warning";
1015 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1016 cameraConfig.getMjpegOptions(),
1017 "http://127.0.0.1:" + cameraConfig.getServerPort() + "/ipcamera.jpg",
1018 cameraConfig.getUser(), cameraConfig.getPassword());
1020 if (ffmpegMjpeg != null) {
1021 ffmpegMjpeg.startConverting();
1025 // if mjpeg stream you can use ffmpeg -i input.h264 -codec:v copy -bsf:v mjpeg2jpeg output%03d.jpg
1026 if (ffmpegSnapshot == null) {
1027 if (inputOptions.isEmpty()) {
1029 inputOptions = "-threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1031 inputOptions = inputOptions + " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1033 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1034 "-an -vsync vfr -update 1",
1035 "http://127.0.0.1:" + cameraConfig.getServerPort() + "/snapshot.jpg",
1036 cameraConfig.getUser(), cameraConfig.getPassword());
1038 if (ffmpegSnapshot != null) {
1039 ffmpegSnapshot.startConverting();
1045 public void noMotionDetected(String thisAlarmsChannel) {
1046 setChannelState(thisAlarmsChannel, OnOffType.OFF);
1047 firstMotionAlarm = false;
1048 motionAlarmUpdateSnapshot = false;
1049 motionDetected = false;
1050 if (streamingAutoFps) {
1051 stopSnapshotPolling();
1052 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1053 stopSnapshotPolling();
1058 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
1059 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
1060 * tampering with the camera.
1062 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
1063 updateState(thisAlarmsChannel, state);
1066 public void motionDetected(String thisAlarmsChannel) {
1067 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
1068 updateState(thisAlarmsChannel, OnOffType.ON);
1069 motionDetected = true;
1070 if (streamingAutoFps) {
1071 startSnapshotPolling();
1073 if (cameraConfig.getUpdateImageWhen().contains("2")) {
1074 if (!firstMotionAlarm) {
1075 if (!snapshotUri.isEmpty()) {
1076 sendHttpGET(snapshotUri);
1078 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1080 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1081 if (!snapshotPolling) {
1082 startSnapshotPolling();
1084 firstMotionAlarm = true;
1085 motionAlarmUpdateSnapshot = true;
1089 public void audioDetected() {
1090 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1091 if (cameraConfig.getUpdateImageWhen().contains("3")) {
1092 if (!firstAudioAlarm) {
1093 if (!snapshotUri.isEmpty()) {
1094 sendHttpGET(snapshotUri);
1096 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1098 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1099 firstAudioAlarm = true;
1100 audioAlarmUpdateSnapshot = true;
1104 public void noAudioDetected() {
1105 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1106 firstAudioAlarm = false;
1107 audioAlarmUpdateSnapshot = false;
1110 public void recordMp4(String filename, int seconds) {
1111 mp4Filename = filename;
1112 mp4RecordTime = seconds;
1113 setupFfmpegFormat(FFmpegFormat.RECORD);
1114 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1117 public void recordGif(String filename, int seconds) {
1118 gifFilename = filename;
1119 if (cameraConfig.getGifPreroll() > 0) {
1120 snapCount = seconds;
1122 setupFfmpegFormat(FFmpegFormat.GIF);
1124 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1127 public String returnValueFromString(String rawString, String searchedString) {
1129 int index = rawString.indexOf(searchedString);
1130 if (index != -1) // -1 means "not found"
1132 result = rawString.substring(index + searchedString.length(), rawString.length());
1133 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1135 return result; // Did not find a carriage return.
1137 return result.substring(0, index);
1140 return ""; // Did not find the String we were searching for
1143 private void sendPTZRequest() {
1144 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1148 public void handleCommand(ChannelUID channelUID, Command command) {
1149 if (command instanceof RefreshType) {
1150 switch (channelUID.getId()) {
1152 if (onvifCamera.supportsPTZ()) {
1153 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1157 if (onvifCamera.supportsPTZ()) {
1158 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1162 if (onvifCamera.supportsPTZ()) {
1163 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1166 case CHANNEL_GOTO_PRESET:
1167 if (onvifCamera.supportsPTZ()) {
1168 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1172 } // caution "REFRESH" can still progress to brand Handlers below the else.
1174 switch (channelUID.getId()) {
1175 case CHANNEL_MP4_HISTORY_LENGTH:
1176 if (DecimalType.ZERO.equals(command)) {
1177 mp4HistoryLength = 0;
1179 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1182 case CHANNEL_GIF_HISTORY_LENGTH:
1183 if (DecimalType.ZERO.equals(command)) {
1184 gifHistoryLength = 0;
1186 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1189 case CHANNEL_FFMPEG_MOTION_CONTROL:
1190 if (OnOffType.ON.equals(command)) {
1191 motionAlarmEnabled = true;
1192 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1193 motionAlarmEnabled = false;
1194 noMotionDetected(CHANNEL_MOTION_ALARM);
1196 motionAlarmEnabled = true;
1197 motionThreshold = Double.valueOf(command.toString());
1198 motionThreshold = motionThreshold / 10000;
1200 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1202 case CHANNEL_START_STREAM:
1203 if (OnOffType.ON.equals(command)) {
1204 setupFfmpegFormat(FFmpegFormat.HLS);
1205 if (ffmpegHLS != null) {
1206 ffmpegHLS.setKeepAlive(-1);// will keep running till manually stopped.
1209 if (ffmpegHLS != null) {
1210 ffmpegHLS.setKeepAlive(1);
1214 case CHANNEL_EXTERNAL_MOTION:
1215 if (OnOffType.ON.equals(command)) {
1216 motionDetected(CHANNEL_EXTERNAL_MOTION);
1218 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1221 case CHANNEL_GOTO_PRESET:
1222 if (onvifCamera.supportsPTZ()) {
1223 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1226 case CHANNEL_POLL_IMAGE:
1227 if (OnOffType.ON.equals(command)) {
1228 if (snapshotUri.isEmpty()) {
1229 ffmpegSnapshotGeneration = true;
1230 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1231 updateImageChannel = false;
1233 updateImageChannel = true;
1234 sendHttpGET(snapshotUri);// Allows this to change Image FPS on demand
1237 if (ffmpegSnapshot != null) {
1238 ffmpegSnapshot.stopConverting();
1239 ffmpegSnapshotGeneration = false;
1241 updateImageChannel = false;
1245 if (onvifCamera.supportsPTZ()) {
1246 if (command instanceof IncreaseDecreaseType) {
1247 if (command == IncreaseDecreaseType.INCREASE) {
1248 if (cameraConfig.getPtzContinuous()) {
1249 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1251 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1254 if (cameraConfig.getPtzContinuous()) {
1255 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1257 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1261 } else if (OnOffType.OFF.equals(command)) {
1262 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1265 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1266 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1270 if (onvifCamera.supportsPTZ()) {
1271 if (command instanceof IncreaseDecreaseType) {
1272 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1273 if (cameraConfig.getPtzContinuous()) {
1274 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1276 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1279 if (cameraConfig.getPtzContinuous()) {
1280 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1282 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1286 } else if (OnOffType.OFF.equals(command)) {
1287 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1290 onvifCamera.setAbsoluteTilt(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.ContinuousMoveIn);
1301 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1304 if (cameraConfig.getPtzContinuous()) {
1305 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1307 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1311 } else if (OnOffType.OFF.equals(command)) {
1312 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1315 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1316 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1321 // commands and refresh now get passed to brand handlers
1322 switch (thing.getThingTypeUID().getId()) {
1324 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1325 amcrestHandler.handleCommand(channelUID, command);
1326 if (lowPriorityRequests.isEmpty()) {
1327 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1331 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1332 dahuaHandler.handleCommand(channelUID, command);
1333 if (lowPriorityRequests.isEmpty()) {
1334 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1337 case DOORBIRD_THING:
1338 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1339 doorBirdHandler.handleCommand(channelUID, command);
1340 if (lowPriorityRequests.isEmpty()) {
1341 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1344 case HIKVISION_THING:
1345 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1346 hikvisionHandler.handleCommand(channelUID, command);
1347 if (lowPriorityRequests.isEmpty()) {
1348 lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1352 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1353 cameraConfig.getPassword());
1354 foscamHandler.handleCommand(channelUID, command);
1355 if (lowPriorityRequests.isEmpty()) {
1356 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1360 InstarHandler instarHandler = new InstarHandler(getHandle());
1361 instarHandler.handleCommand(channelUID, command);
1362 if (lowPriorityRequests.isEmpty()) {
1363 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1367 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1368 defaultHandler.handleCommand(channelUID, command);
1369 if (lowPriorityRequests.isEmpty()) {
1370 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1376 public void setChannelState(String channelToUpdate, State valueOf) {
1377 updateState(channelToUpdate, valueOf);
1380 void bringCameraOnline() {
1382 updateStatus(ThingStatus.ONLINE);
1383 groupTracker.listOfOnlineCameraHandlers.add(this);
1384 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1385 if (cameraConnectionJob != null) {
1386 cameraConnectionJob.cancel(false);
1389 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1390 snapshotPolling = true;
1391 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 1000, cameraConfig.getPollTime(),
1392 TimeUnit.MILLISECONDS);
1395 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1397 if (!rtspUri.isEmpty()) {
1398 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1400 if (updateImageChannel) {
1401 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1403 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1405 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1406 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1407 handle.cameraOnline(getThing().getUID().getId());
1412 void snapshotIsFfmpeg() {
1413 bringCameraOnline();
1414 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1416 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1417 if (!rtspUri.isEmpty()) {
1418 updateImageChannel = false;
1419 ffmpegSnapshotGeneration = true;
1420 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1421 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1423 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1427 void pollingCameraConnection() {
1428 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1429 if (rtspUri.isEmpty()) {
1430 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1432 if (snapshotUri.isEmpty() || snapshotUri.equals("ffmpeg")) {
1435 sendHttpRequest("GET", snapshotUri, null);
1439 if (!onvifCamera.isConnected()) {
1440 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1441 cameraConfig.getOnvifPort());
1442 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1444 if (snapshotUri.equals("ffmpeg")) {
1446 } else if (!snapshotUri.isEmpty()) {
1447 sendHttpRequest("GET", snapshotUri, null);
1448 } else if (!rtspUri.isEmpty()) {
1451 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1452 "Camera failed to report a valid Snaphot and/or RTSP URL. See readme on how to use the SNAPSHOT_URL_OVERRIDE feature.");
1456 public void cameraConfigError(String reason) {
1457 // wont try to reconnect again due to a config error being the cause.
1458 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1462 public void cameraCommunicationError(String reason) {
1463 // will try to reconnect again as camera may be rebooting.
1464 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1465 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1466 resetAndRetryConnecting();
1470 boolean streamIsStopped(String url) {
1471 ChannelTracking channelTracking = channelTrackingMap.get(url);
1472 if (channelTracking != null) {
1473 if (channelTracking.getChannel().isOpen()) {
1474 return false; // stream is running.
1477 return true; // Stream stopped or never started.
1480 void snapshotRunnable() {
1481 // Snapshot should be first to keep consistent time between shots
1482 sendHttpGET(snapshotUri);
1483 if (snapCount > 0) {
1484 if (--snapCount == 0) {
1485 setupFfmpegFormat(FFmpegFormat.GIF);
1490 public void stopSnapshotPolling() {
1491 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1492 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1493 snapshotPolling = false;
1494 if (snapshotJob != null) {
1495 snapshotJob.cancel(true);
1497 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1498 snapshotPolling = false;
1499 if (snapshotJob != null) {
1500 snapshotJob.cancel(true);
1505 public void startSnapshotPolling() {
1506 if (snapshotPolling || ffmpegSnapshotGeneration) {
1507 return; // Already polling or creating with FFmpeg from RTSP
1509 if (streamingSnapshotMjpeg || streamingAutoFps) {
1510 snapshotPolling = true;
1511 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1512 TimeUnit.MILLISECONDS);
1513 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1514 snapshotPolling = true;
1515 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1516 TimeUnit.MILLISECONDS);
1520 // runs every 8 seconds due to mjpeg streams not staying open unless they update this often.
1521 void pollCameraRunnable() {
1522 // Snapshot should be first to keep consistent time between shots
1523 if (!snapshotUri.isEmpty()) {
1524 if (updateImageChannel) {
1525 sendHttpGET(snapshotUri);
1528 if (streamingAutoFps) {
1529 updateAutoFps = true;
1530 if (!snapshotPolling && !ffmpegSnapshotGeneration) {
1531 // Dont need to poll if creating from RTSP stream with FFmpeg or we are polling at full rate already.
1532 sendHttpGET(snapshotUri);
1535 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1536 if (!lowPriorityRequests.isEmpty()) {
1537 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1538 lowPriorityCounter = 0;
1540 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1542 // what needs to be done every poll//
1543 switch (thing.getThingTypeUID().getId()) {
1547 if (!onvifCamera.isConnected()) {
1548 onvifCamera.connect(true);
1552 noMotionDetected(CHANNEL_MOTION_ALARM);
1553 noMotionDetected(CHANNEL_PIR_ALARM);
1556 case HIKVISION_THING:
1557 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1558 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1559 cameraConfig.getIp());
1560 sendHttpGET("/ISAPI/Event/notification/alertStream");
1564 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1565 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1568 // Check for alarms, channel for NVRs appears not to work at filtering.
1569 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1570 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1571 cameraConfig.getIp());
1572 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1575 case DOORBIRD_THING:
1576 // Check for alarms, channel for NVRs appears not to work at filtering.
1577 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1578 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1579 cameraConfig.getIp());
1580 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1584 if (ffmpegHLS != null) {
1585 ffmpegHLS.checkKeepAlive();
1587 if (openChannels.size() > 18) {
1588 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1594 public void initialize() {
1595 cameraConfig = getConfigAs(CameraConfig.class);
1596 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1597 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1598 rtspUri = cameraConfig.getFfmpegInput();
1600 if (cameraConfig.getServerPort() < 1) {
1602 "The Server Port is not set to a valid number which disables a lot of binding features. See readme for more info.");
1603 } else if (cameraConfig.getServerPort() < 1025) {
1604 logger.warn("The Server Port is <= 1024 and may cause permission errors under Linux, try a higher number.");
1607 // Known cameras will connect quicker if we skip ONVIF questions.
1608 switch (thing.getThingTypeUID().getId()) {
1611 if (mjpegUri.isEmpty()) {
1612 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1614 if (snapshotUri.isEmpty()) {
1615 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1618 case DOORBIRD_THING:
1619 if (mjpegUri.isEmpty()) {
1620 mjpegUri = "/bha-api/video.cgi";
1622 if (snapshotUri.isEmpty()) {
1623 snapshotUri = "/bha-api/image.cgi";
1627 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1628 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1629 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1630 if (mjpegUri.isEmpty()) {
1631 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1632 + cameraConfig.getPassword();
1634 if (snapshotUri.isEmpty()) {
1635 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1636 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1639 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1640 if (mjpegUri.isEmpty()) {
1641 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1643 if (snapshotUri.isEmpty()) {
1644 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1648 if (snapshotUri.isEmpty()) {
1649 snapshotUri = "/tmpfs/snap.jpg";
1651 if (mjpegUri.isEmpty()) {
1652 mjpegUri = "/mjpegstream.cgi?-chn=12";
1657 // Onvif and Instar event handling needs the host IP and the server started.
1658 if (cameraConfig.getServerPort() > 0) {
1659 startStreamServer();
1662 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1663 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1664 cameraConfig.getUser(), cameraConfig.getPassword());
1665 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1666 // Only use ONVIF events if it is not an API camera.
1667 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1670 // for poll times above 9 seconds don't display a warning about the Image channel.
1671 if (9000 <= cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1673 "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.");
1675 // Waiting 3 seconds for ONVIF to discover the urls before running.
1676 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 30, TimeUnit.SECONDS);
1679 // What the camera needs to re-connect if the initialize() is not called.
1680 private void resetAndRetryConnecting() {
1686 public void dispose() {
1688 snapshotPolling = false;
1689 onvifCamera.disconnect();
1690 if (pollCameraJob != null) {
1691 pollCameraJob.cancel(true);
1692 pollCameraJob = null;
1694 if (snapshotJob != null) {
1695 snapshotJob.cancel(true);
1698 if (cameraConnectionJob != null) {
1699 cameraConnectionJob.cancel(true);
1700 cameraConnectionJob = null;
1702 threadPool.shutdown();
1703 threadPool = Executors.newScheduledThreadPool(4);
1705 groupTracker.listOfOnlineCameraHandlers.remove(this);
1706 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1707 // inform all group handlers that this camera has gone offline
1708 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1709 handle.cameraOffline(this);
1711 basicAuth = ""; // clear out stored Password hash
1712 useDigestAuth = false;
1714 openChannels.close();
1716 if (ffmpegHLS != null) {
1717 ffmpegHLS.stopConverting();
1720 if (ffmpegRecord != null) {
1721 ffmpegRecord.stopConverting();
1722 ffmpegRecord = null;
1724 if (ffmpegGIF != null) {
1725 ffmpegGIF.stopConverting();
1728 if (ffmpegRtspHelper != null) {
1729 ffmpegRtspHelper.stopConverting();
1730 ffmpegRtspHelper = null;
1732 if (ffmpegMjpeg != null) {
1733 ffmpegMjpeg.stopConverting();
1736 if (ffmpegSnapshot != null) {
1737 ffmpegSnapshot.stopConverting();
1738 ffmpegSnapshot = null;
1740 channelTrackingMap.clear();
1743 public void setStreamServerHandler(StreamServerHandler streamServerHandler2) {
1744 streamServerHandler = streamServerHandler2;
1747 public String getWhiteList() {
1748 return cameraConfig.getIpWhitelist();
1752 public Collection<Class<? extends ThingHandlerService>> getServices() {
1753 return Collections.singleton(IpCameraActions.class);