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("Host", cameraConfig.getIp() + ":" + port);
556 request.headers().set("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("Authorization", "Basic " + basicAuth);
571 if (digestString != null) {
572 request.headers().set("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 gifRecordTime = seconds;
1120 if (cameraConfig.getGifPreroll() > 0) {
1121 snapCount = seconds;
1123 setupFfmpegFormat(FFmpegFormat.GIF);
1125 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1128 public String returnValueFromString(String rawString, String searchedString) {
1130 int index = rawString.indexOf(searchedString);
1131 if (index != -1) // -1 means "not found"
1133 result = rawString.substring(index + searchedString.length(), rawString.length());
1134 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1136 return result; // Did not find a carriage return.
1138 return result.substring(0, index);
1141 return ""; // Did not find the String we were searching for
1144 private void sendPTZRequest() {
1145 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1149 public void handleCommand(ChannelUID channelUID, Command command) {
1150 if (command instanceof RefreshType) {
1151 switch (channelUID.getId()) {
1153 if (onvifCamera.supportsPTZ()) {
1154 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1158 if (onvifCamera.supportsPTZ()) {
1159 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1163 if (onvifCamera.supportsPTZ()) {
1164 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1167 case CHANNEL_GOTO_PRESET:
1168 if (onvifCamera.supportsPTZ()) {
1169 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1173 } // caution "REFRESH" can still progress to brand Handlers below the else.
1175 switch (channelUID.getId()) {
1176 case CHANNEL_MP4_HISTORY_LENGTH:
1177 if (DecimalType.ZERO.equals(command)) {
1178 mp4HistoryLength = 0;
1180 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1183 case CHANNEL_GIF_HISTORY_LENGTH:
1184 if (DecimalType.ZERO.equals(command)) {
1185 gifHistoryLength = 0;
1187 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1190 case CHANNEL_FFMPEG_MOTION_CONTROL:
1191 if (OnOffType.ON.equals(command)) {
1192 motionAlarmEnabled = true;
1193 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1194 motionAlarmEnabled = false;
1195 noMotionDetected(CHANNEL_MOTION_ALARM);
1197 motionAlarmEnabled = true;
1198 motionThreshold = Double.valueOf(command.toString());
1199 motionThreshold = motionThreshold / 10000;
1201 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1203 case CHANNEL_START_STREAM:
1204 if (OnOffType.ON.equals(command)) {
1205 setupFfmpegFormat(FFmpegFormat.HLS);
1206 if (ffmpegHLS != null) {
1207 ffmpegHLS.setKeepAlive(-1);// will keep running till manually stopped.
1210 if (ffmpegHLS != null) {
1211 ffmpegHLS.setKeepAlive(1);
1215 case CHANNEL_EXTERNAL_MOTION:
1216 if (OnOffType.ON.equals(command)) {
1217 motionDetected(CHANNEL_EXTERNAL_MOTION);
1219 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1222 case CHANNEL_GOTO_PRESET:
1223 if (onvifCamera.supportsPTZ()) {
1224 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1227 case CHANNEL_POLL_IMAGE:
1228 if (OnOffType.ON.equals(command)) {
1229 if (snapshotUri.isEmpty()) {
1230 ffmpegSnapshotGeneration = true;
1231 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1232 updateImageChannel = false;
1234 updateImageChannel = true;
1235 sendHttpGET(snapshotUri);// Allows this to change Image FPS on demand
1238 if (ffmpegSnapshot != null) {
1239 ffmpegSnapshot.stopConverting();
1240 ffmpegSnapshotGeneration = false;
1242 updateImageChannel = false;
1246 if (onvifCamera.supportsPTZ()) {
1247 if (command instanceof IncreaseDecreaseType) {
1248 if (command == IncreaseDecreaseType.INCREASE) {
1249 if (cameraConfig.getPtzContinuous()) {
1250 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1252 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1255 if (cameraConfig.getPtzContinuous()) {
1256 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1258 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1262 } else if (OnOffType.OFF.equals(command)) {
1263 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1266 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1267 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1271 if (onvifCamera.supportsPTZ()) {
1272 if (command instanceof IncreaseDecreaseType) {
1273 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1274 if (cameraConfig.getPtzContinuous()) {
1275 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1277 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1280 if (cameraConfig.getPtzContinuous()) {
1281 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1283 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1287 } else if (OnOffType.OFF.equals(command)) {
1288 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1291 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1292 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1296 if (onvifCamera.supportsPTZ()) {
1297 if (command instanceof IncreaseDecreaseType) {
1298 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1299 if (cameraConfig.getPtzContinuous()) {
1300 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1302 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1305 if (cameraConfig.getPtzContinuous()) {
1306 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1308 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1312 } else if (OnOffType.OFF.equals(command)) {
1313 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1316 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1317 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1322 // commands and refresh now get passed to brand handlers
1323 switch (thing.getThingTypeUID().getId()) {
1325 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1326 amcrestHandler.handleCommand(channelUID, command);
1327 if (lowPriorityRequests.isEmpty()) {
1328 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1332 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1333 dahuaHandler.handleCommand(channelUID, command);
1334 if (lowPriorityRequests.isEmpty()) {
1335 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1338 case DOORBIRD_THING:
1339 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1340 doorBirdHandler.handleCommand(channelUID, command);
1341 if (lowPriorityRequests.isEmpty()) {
1342 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1345 case HIKVISION_THING:
1346 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1347 hikvisionHandler.handleCommand(channelUID, command);
1348 if (lowPriorityRequests.isEmpty()) {
1349 lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1353 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1354 cameraConfig.getPassword());
1355 foscamHandler.handleCommand(channelUID, command);
1356 if (lowPriorityRequests.isEmpty()) {
1357 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1361 InstarHandler instarHandler = new InstarHandler(getHandle());
1362 instarHandler.handleCommand(channelUID, command);
1363 if (lowPriorityRequests.isEmpty()) {
1364 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1368 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1369 defaultHandler.handleCommand(channelUID, command);
1370 if (lowPriorityRequests.isEmpty()) {
1371 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1377 public void setChannelState(String channelToUpdate, State valueOf) {
1378 updateState(channelToUpdate, valueOf);
1381 void bringCameraOnline() {
1383 updateStatus(ThingStatus.ONLINE);
1384 groupTracker.listOfOnlineCameraHandlers.add(this);
1385 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1386 if (cameraConnectionJob != null) {
1387 cameraConnectionJob.cancel(false);
1390 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1391 snapshotPolling = true;
1392 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 1000, cameraConfig.getPollTime(),
1393 TimeUnit.MILLISECONDS);
1396 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1398 if (!rtspUri.isEmpty()) {
1399 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1401 if (updateImageChannel) {
1402 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1404 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1406 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1407 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1408 handle.cameraOnline(getThing().getUID().getId());
1413 void snapshotIsFfmpeg() {
1414 bringCameraOnline();
1415 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1417 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1418 if (!rtspUri.isEmpty()) {
1419 updateImageChannel = false;
1420 ffmpegSnapshotGeneration = true;
1421 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1422 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1424 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1428 void pollingCameraConnection() {
1429 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1430 if (rtspUri.isEmpty()) {
1431 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1433 if (snapshotUri.isEmpty() || snapshotUri.equals("ffmpeg")) {
1436 sendHttpRequest("GET", snapshotUri, null);
1440 if (!onvifCamera.isConnected()) {
1441 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1442 cameraConfig.getOnvifPort());
1443 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1445 if (snapshotUri.equals("ffmpeg")) {
1447 } else if (!snapshotUri.isEmpty()) {
1448 sendHttpRequest("GET", snapshotUri, null);
1449 } else if (!rtspUri.isEmpty()) {
1452 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1453 "Camera failed to report a valid Snaphot and/or RTSP URL. See readme on how to use the SNAPSHOT_URL_OVERRIDE feature.");
1457 public void cameraConfigError(String reason) {
1458 // wont try to reconnect again due to a config error being the cause.
1459 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1463 public void cameraCommunicationError(String reason) {
1464 // will try to reconnect again as camera may be rebooting.
1465 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1466 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1467 resetAndRetryConnecting();
1471 boolean streamIsStopped(String url) {
1472 ChannelTracking channelTracking = channelTrackingMap.get(url);
1473 if (channelTracking != null) {
1474 if (channelTracking.getChannel().isOpen()) {
1475 return false; // stream is running.
1478 return true; // Stream stopped or never started.
1481 void snapshotRunnable() {
1482 // Snapshot should be first to keep consistent time between shots
1483 sendHttpGET(snapshotUri);
1484 if (snapCount > 0) {
1485 if (--snapCount == 0) {
1486 setupFfmpegFormat(FFmpegFormat.GIF);
1491 public void stopSnapshotPolling() {
1492 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1493 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1494 snapshotPolling = false;
1495 if (snapshotJob != null) {
1496 snapshotJob.cancel(true);
1498 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1499 snapshotPolling = false;
1500 if (snapshotJob != null) {
1501 snapshotJob.cancel(true);
1506 public void startSnapshotPolling() {
1507 if (snapshotPolling || ffmpegSnapshotGeneration) {
1508 return; // Already polling or creating with FFmpeg from RTSP
1510 if (streamingSnapshotMjpeg || streamingAutoFps) {
1511 snapshotPolling = true;
1512 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1513 TimeUnit.MILLISECONDS);
1514 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1515 snapshotPolling = true;
1516 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1517 TimeUnit.MILLISECONDS);
1521 // runs every 8 seconds due to mjpeg streams not staying open unless they update this often.
1522 void pollCameraRunnable() {
1523 // Snapshot should be first to keep consistent time between shots
1524 if (!snapshotUri.isEmpty()) {
1525 if (updateImageChannel) {
1526 sendHttpGET(snapshotUri);
1529 if (streamingAutoFps) {
1530 updateAutoFps = true;
1531 if (!snapshotPolling && !ffmpegSnapshotGeneration) {
1532 // Dont need to poll if creating from RTSP stream with FFmpeg or we are polling at full rate already.
1533 sendHttpGET(snapshotUri);
1536 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1537 if (!lowPriorityRequests.isEmpty()) {
1538 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1539 lowPriorityCounter = 0;
1541 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1543 // what needs to be done every poll//
1544 switch (thing.getThingTypeUID().getId()) {
1548 if (!onvifCamera.isConnected()) {
1549 onvifCamera.connect(true);
1553 noMotionDetected(CHANNEL_MOTION_ALARM);
1554 noMotionDetected(CHANNEL_PIR_ALARM);
1557 case HIKVISION_THING:
1558 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1559 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1560 cameraConfig.getIp());
1561 sendHttpGET("/ISAPI/Event/notification/alertStream");
1565 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1566 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1569 // Check for alarms, channel for NVRs appears not to work at filtering.
1570 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1571 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1572 cameraConfig.getIp());
1573 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1576 case DOORBIRD_THING:
1577 // Check for alarms, channel for NVRs appears not to work at filtering.
1578 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1579 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1580 cameraConfig.getIp());
1581 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1585 if (ffmpegHLS != null) {
1586 ffmpegHLS.checkKeepAlive();
1588 if (openChannels.size() > 18) {
1589 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1595 public void initialize() {
1596 cameraConfig = getConfigAs(CameraConfig.class);
1597 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1598 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1599 rtspUri = cameraConfig.getFfmpegInput();
1601 if (cameraConfig.getServerPort() < 1) {
1603 "The Server Port is not set to a valid number which disables a lot of binding features. See readme for more info.");
1604 } else if (cameraConfig.getServerPort() < 1025) {
1605 logger.warn("The Server Port is <= 1024 and may cause permission errors under Linux, try a higher number.");
1608 // Known cameras will connect quicker if we skip ONVIF questions.
1609 switch (thing.getThingTypeUID().getId()) {
1612 if (mjpegUri.isEmpty()) {
1613 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1615 if (snapshotUri.isEmpty()) {
1616 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1619 case DOORBIRD_THING:
1620 if (mjpegUri.isEmpty()) {
1621 mjpegUri = "/bha-api/video.cgi";
1623 if (snapshotUri.isEmpty()) {
1624 snapshotUri = "/bha-api/image.cgi";
1628 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1629 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1630 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1631 if (mjpegUri.isEmpty()) {
1632 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1633 + cameraConfig.getPassword();
1635 if (snapshotUri.isEmpty()) {
1636 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1637 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1640 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1641 if (mjpegUri.isEmpty()) {
1642 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1644 if (snapshotUri.isEmpty()) {
1645 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1649 if (snapshotUri.isEmpty()) {
1650 snapshotUri = "/tmpfs/snap.jpg";
1652 if (mjpegUri.isEmpty()) {
1653 mjpegUri = "/mjpegstream.cgi?-chn=12";
1658 // Onvif and Instar event handling needs the host IP and the server started.
1659 if (cameraConfig.getServerPort() > 0) {
1660 startStreamServer();
1663 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1664 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1665 cameraConfig.getUser(), cameraConfig.getPassword());
1666 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1667 // Only use ONVIF events if it is not an API camera.
1668 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1671 // for poll times above 9 seconds don't display a warning about the Image channel.
1672 if (9000 <= cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1674 "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.");
1676 // Waiting 3 seconds for ONVIF to discover the urls before running.
1677 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 30, TimeUnit.SECONDS);
1680 // What the camera needs to re-connect if the initialize() is not called.
1681 private void resetAndRetryConnecting() {
1687 public void dispose() {
1689 snapshotPolling = false;
1690 onvifCamera.disconnect();
1691 if (pollCameraJob != null) {
1692 pollCameraJob.cancel(true);
1693 pollCameraJob = null;
1695 if (snapshotJob != null) {
1696 snapshotJob.cancel(true);
1699 if (cameraConnectionJob != null) {
1700 cameraConnectionJob.cancel(true);
1701 cameraConnectionJob = null;
1703 threadPool.shutdown();
1704 threadPool = Executors.newScheduledThreadPool(4);
1706 groupTracker.listOfOnlineCameraHandlers.remove(this);
1707 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1708 // inform all group handlers that this camera has gone offline
1709 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1710 handle.cameraOffline(this);
1712 basicAuth = ""; // clear out stored Password hash
1713 useDigestAuth = false;
1715 openChannels.close();
1717 if (ffmpegHLS != null) {
1718 ffmpegHLS.stopConverting();
1721 if (ffmpegRecord != null) {
1722 ffmpegRecord.stopConverting();
1723 ffmpegRecord = null;
1725 if (ffmpegGIF != null) {
1726 ffmpegGIF.stopConverting();
1729 if (ffmpegRtspHelper != null) {
1730 ffmpegRtspHelper.stopConverting();
1731 ffmpegRtspHelper = null;
1733 if (ffmpegMjpeg != null) {
1734 ffmpegMjpeg.stopConverting();
1737 if (ffmpegSnapshot != null) {
1738 ffmpegSnapshot.stopConverting();
1739 ffmpegSnapshot = null;
1741 channelTrackingMap.clear();
1744 public void setStreamServerHandler(StreamServerHandler streamServerHandler2) {
1745 streamServerHandler = streamServerHandler2;
1748 public String getWhiteList() {
1749 return cameraConfig.getIpWhitelist();
1753 public Collection<Class<? extends ThingHandlerService>> getServices() {
1754 return Collections.singleton(IpCameraActions.class);