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 LinkedList<byte[]> fifoSnapshotBuffer = new LinkedList<byte[]>();
165 private int snapCount;
166 private boolean updateImageChannel = false;
167 private boolean updateAutoFps = false;
168 private byte lowPriorityCounter = 0;
169 public String hostIp;
170 public Map<String, ChannelTracking> channelTrackingMap = new ConcurrentHashMap<>();
171 public List<String> lowPriorityRequests = new ArrayList<>(0);
173 // basicAuth MUST remain private as it holds the cameraConfig.getPassword()
174 private String basicAuth = "";
175 public boolean useBasicAuth = false;
176 public boolean useDigestAuth = false;
177 public String snapshotUri = "";
178 public String mjpegUri = "";
179 private @Nullable ChannelFuture serverFuture = null;
180 private Object firstStreamedMsg = new Object();
181 public byte[] currentSnapshot = new byte[] { (byte) 0x00 };
182 public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
183 public String rtspUri = "";
184 public boolean audioAlarmUpdateSnapshot = false;
185 private boolean motionAlarmUpdateSnapshot = false;
186 private boolean isOnline = false; // Used so only 1 error is logged when a network issue occurs.
187 private boolean firstAudioAlarm = false;
188 private boolean firstMotionAlarm = false;
189 public Double motionThreshold = 0.0016;
190 public int audioThreshold = 35;
191 @SuppressWarnings("unused")
192 private @Nullable StreamServerHandler streamServerHandler;
193 private boolean streamingSnapshotMjpeg = false;
194 public boolean motionAlarmEnabled = false;
195 public boolean audioAlarmEnabled = false;
196 public boolean ffmpegSnapshotGeneration = false;
197 public boolean snapshotPolling = false;
198 public OnvifConnection onvifCamera = new OnvifConnection(this, "", "", "");
200 // These methods handle the response from all camera brands, nothing specific to 1 brand.
201 private class CommonCameraHandler extends ChannelDuplexHandler {
202 private int bytesToRecieve = 0;
203 private int bytesAlreadyRecieved = 0;
204 private byte[] incomingJpeg = new byte[0];
205 private String incomingMessage = "";
206 private String contentType = "empty";
207 private Object reply = new Object();
208 private String requestUrl = "";
209 private boolean closeConnection = true;
210 private boolean isChunked = false;
212 public void setURL(String url) {
217 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
218 if (msg == null || ctx == null) {
222 if (msg instanceof HttpResponse) {
223 HttpResponse response = (HttpResponse) msg;
224 if (response.status().code() != 401) {
225 if (!response.headers().isEmpty()) {
226 for (String name : response.headers().names()) {
227 // Some cameras use first letter uppercase and others dont.
228 switch (name.toLowerCase()) { // Possible localization issues doing this
230 contentType = response.headers().getAsString(name);
232 case "content-length":
233 bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
236 if (response.headers().getAsString(name).contains("keep-alive")) {
237 closeConnection = false;
240 case "transfer-encoding":
241 if (response.headers().getAsString(name).contains("chunked")) {
247 if (contentType.contains("multipart")) {
248 closeConnection = false;
249 if (mjpegUri.contains(requestUrl)) {
250 if (msg instanceof HttpMessage) {
251 // very start of stream only
252 ReferenceCountUtil.retain(msg, 1);
253 firstStreamedMsg = msg;
254 streamToGroup(firstStreamedMsg, mjpegChannelGroup, true);
257 } else if (contentType.contains("image/jp")) {
258 if (bytesToRecieve == 0) {
259 bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
260 logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
262 incomingJpeg = new byte[bytesToRecieve];
267 if (msg instanceof HttpContent) {
268 if (mjpegUri.contains(requestUrl)) {
269 // multiple MJPEG stream packets come back as this.
270 ReferenceCountUtil.retain(msg, 1);
271 streamToGroup(msg, mjpegChannelGroup, true);
273 HttpContent content = (HttpContent) msg;
274 // Found some cameras uses Content-Type: image/jpg instead of image/jpeg
275 if (contentType.contains("image/jp")) {
276 for (int i = 0; i < content.content().capacity(); i++) {
277 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
279 if (content instanceof LastHttpContent) {
280 processSnapshot(incomingJpeg);
281 // testing next line and if works need to do a full cleanup of this function.
282 closeConnection = true;
283 if (closeConnection) {
287 bytesAlreadyRecieved = 0;
290 } else { // incomingMessage that is not an IMAGE
291 if (incomingMessage.isEmpty()) {
292 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
294 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
296 bytesAlreadyRecieved = incomingMessage.length();
297 if (content instanceof LastHttpContent) {
298 // If it is not an image send it on to the next handler//
299 if (bytesAlreadyRecieved != 0) {
300 reply = incomingMessage;
301 super.channelRead(ctx, reply);
304 // HIKVISION alertStream never has a LastHttpContent as it always stays open//
305 if (contentType.contains("multipart")) {
306 if (bytesAlreadyRecieved != 0) {
307 reply = incomingMessage;
308 incomingMessage = "";
310 bytesAlreadyRecieved = 0;
311 super.channelRead(ctx, reply);
314 // Foscam needs this as will other cameras with chunks//
315 if (isChunked && bytesAlreadyRecieved != 0) {
316 reply = incomingMessage;
317 super.channelRead(ctx, reply);
321 } else { // msg is not HttpContent
322 // Foscam and Amcrest cameras need this
323 if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
324 reply = incomingMessage;
325 logger.debug("Packet back from camera is {}", incomingMessage);
326 super.channelRead(ctx, reply);
330 ReferenceCountUtil.release(msg);
335 public void channelReadComplete(@Nullable ChannelHandlerContext ctx) {
339 public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
343 public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
347 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
348 if (cause == null || ctx == null) {
351 if (cause instanceof ArrayIndexOutOfBoundsException) {
352 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
355 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
362 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
366 if (evt instanceof IdleStateEvent) {
367 IdleStateEvent e = (IdleStateEvent) evt;
368 // If camera does not use the channel for X amount of time it will close.
369 if (e.state() == IdleState.READER_IDLE) {
370 String urlToKeepOpen = "";
371 switch (thing.getThingTypeUID().getId()) {
373 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
375 case HIKVISION_THING:
376 urlToKeepOpen = "/ISAPI/Event/notification/alertStream";
379 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
382 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
383 if (channelTracking != null) {
384 if (channelTracking.getChannel() == ctx.channel()) {
385 return; // don't auto close this as it is for the alarms.
394 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker) {
396 cameraConfig = getConfigAs(CameraConfig.class);
397 if (ipAddress != null) {
400 hostIp = Helper.getLocalIpAddress();
402 this.groupTracker = groupTracker;
405 private IpCameraHandler getHandle() {
409 // false clears the stored user/pass hash, true creates the hash
410 public boolean setBasicAuth(boolean useBasic) {
412 logger.debug("Clearing out the stored BASIC auth now.");
415 } else if (!basicAuth.isEmpty()) {
416 // due to camera may have been sent multiple requests before the auth was set, this may trigger falsely.
417 logger.warn("Camera is reporting your username and/or password is wrong.");
420 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
421 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
422 ByteBuf byteBuf = null;
424 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
425 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
427 if (byteBuf != null) {
433 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
438 private String getCorrectUrlFormat(String longUrl) {
439 String temp = longUrl;
442 if (longUrl.isEmpty() || longUrl.equals("ffmpeg")) {
447 url = new URL(longUrl);
448 int port = url.getPort();
450 if (url.getQuery() == null) {
451 temp = url.getPath();
453 temp = url.getPath() + "?" + url.getQuery();
456 if (url.getQuery() == null) {
457 temp = ":" + url.getPort() + url.getPath();
459 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
462 } catch (MalformedURLException e) {
463 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
468 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
469 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
470 sendHttpRequest("PUT", httpRequestURL, null);
473 public void sendHttpGET(String httpRequestURL) {
474 sendHttpRequest("GET", httpRequestURL, null);
477 public int getPortFromShortenedUrl(String httpRequestURL) {
478 if (httpRequestURL.startsWith(":")) {
479 int end = httpRequestURL.indexOf("/");
480 return Integer.parseInt(httpRequestURL.substring(1, end));
482 return cameraConfig.getPort();
485 public String getTinyUrl(String httpRequestURL) {
486 if (httpRequestURL.startsWith(":")) {
487 int beginIndex = httpRequestURL.indexOf("/");
488 return httpRequestURL.substring(beginIndex);
490 return httpRequestURL;
493 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
494 // The authHandler will generate a digest string and re-send using this same function when needed.
495 @SuppressWarnings("null")
496 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
497 int port = getPortFromShortenedUrl(httpRequestURLFull);
498 String httpRequestURL = getTinyUrl(httpRequestURLFull);
500 if (mainBootstrap == null) {
501 mainBootstrap = new Bootstrap();
502 mainBootstrap.group(mainEventLoopGroup);
503 mainBootstrap.channel(NioSocketChannel.class);
504 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
505 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
506 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
507 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
508 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
509 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
512 public void initChannel(SocketChannel socketChannel) throws Exception {
513 // HIK Alarm stream needs > 9sec idle to stop stream closing
514 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
515 socketChannel.pipeline().addLast(new HttpClientCodec());
516 socketChannel.pipeline().addLast(AUTH_HANDLER,
517 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
518 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
520 switch (thing.getThingTypeUID().getId()) {
522 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
525 socketChannel.pipeline()
526 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
529 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
532 socketChannel.pipeline().addLast(
533 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
535 case HIKVISION_THING:
536 socketChannel.pipeline()
537 .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
540 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
543 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
550 FullHttpRequest request;
551 if (!"PUT".equals(httpMethod) || (useDigestAuth && digestString == null)) {
552 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
553 request.headers().set("Host", cameraConfig.getIp() + ":" + port);
554 request.headers().set("Connection", HttpHeaderValues.KEEP_ALIVE);
556 request = putRequestWithBody;
559 if (!basicAuth.isEmpty()) {
561 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
564 request.headers().set("Authorization", "Basic " + basicAuth);
569 if (digestString != null) {
570 request.headers().set("Authorization", "Digest " + digestString);
574 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
575 .addListener(new ChannelFutureListener() {
578 public void operationComplete(@Nullable ChannelFuture future) {
579 if (future == null) {
582 if (future.isDone() && future.isSuccess()) {
583 Channel ch = future.channel();
584 openChannels.add(ch);
588 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
590 channelTrackingMap.put(httpRequestURL, new ChannelTracking(ch, httpRequestURL));
592 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
593 commonHandler.setURL(httpRequestURLFull);
594 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
595 authHandler.setURL(httpMethod, httpRequestURL);
597 switch (thing.getThingTypeUID().getId()) {
599 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
600 amcrestHandler.setURL(httpRequestURL);
603 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
604 instarHandler.setURL(httpRequestURL);
607 ch.writeAndFlush(request);
608 } else { // an error occured
609 cameraCommunicationError(
610 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
616 public void processSnapshot(byte[] incommingSnapshot) {
617 lockCurrentSnapshot.lock();
619 currentSnapshot = incommingSnapshot;
620 if (cameraConfig.getGifPreroll() > 0) {
621 fifoSnapshotBuffer.add(incommingSnapshot);
622 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
623 fifoSnapshotBuffer.removeFirst();
627 lockCurrentSnapshot.unlock();
630 if (streamingSnapshotMjpeg) {
631 sendMjpegFrame(incommingSnapshot, snapshotMjpegChannelGroup);
633 if (streamingAutoFps) {
634 if (motionDetected) {
635 sendMjpegFrame(incommingSnapshot, autoSnapshotMjpegChannelGroup);
636 } else if (updateAutoFps) {
637 // only happens every 8 seconds as some browsers need a frame that often to keep stream alive.
638 sendMjpegFrame(incommingSnapshot, autoSnapshotMjpegChannelGroup);
639 updateAutoFps = false;
643 if (updateImageChannel) {
644 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
645 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
646 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
647 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
648 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
649 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
650 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
654 public void stopStreamServer() {
655 serversLoopGroup.shutdownGracefully();
656 serverBootstrap = null;
659 @SuppressWarnings("null")
660 public void startStreamServer() {
661 if (serverBootstrap == null) {
663 serversLoopGroup = new NioEventLoopGroup();
664 serverBootstrap = new ServerBootstrap();
665 serverBootstrap.group(serversLoopGroup);
666 serverBootstrap.channel(NioServerSocketChannel.class);
667 // IP "0.0.0.0" will bind the server to all network connections//
668 serverBootstrap.localAddress(new InetSocketAddress("0.0.0.0", cameraConfig.getServerPort()));
669 serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
671 protected void initChannel(SocketChannel socketChannel) throws Exception {
672 socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 60, 0));
673 socketChannel.pipeline().addLast("HttpServerCodec", new HttpServerCodec());
674 socketChannel.pipeline().addLast("ChunkedWriteHandler", new ChunkedWriteHandler());
675 socketChannel.pipeline().addLast("streamServerHandler", new StreamServerHandler(getHandle()));
678 serverFuture = serverBootstrap.bind().sync();
679 serverFuture.await(4000);
680 logger.debug("File server for camera at {} has started on port {} for all NIC's.", cameraConfig.getIp(),
681 cameraConfig.getServerPort());
682 updateState(CHANNEL_MJPEG_URL,
683 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.mjpeg"));
684 updateState(CHANNEL_HLS_URL,
685 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.m3u8"));
686 updateState(CHANNEL_IMAGE_URL,
687 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.jpg"));
688 } catch (Exception e) {
689 cameraConfigError("Exception when starting server. Try changing the Server Port to another number.");
694 public void setupSnapshotStreaming(boolean stream, ChannelHandlerContext ctx, boolean auto) {
696 sendMjpegFirstPacket(ctx);
698 autoSnapshotMjpegChannelGroup.add(ctx.channel());
699 lockCurrentSnapshot.lock();
701 sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
702 // iOS uses a FIFO? and needs two frames to display a pic
703 sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
705 lockCurrentSnapshot.unlock();
707 streamingAutoFps = true;
709 snapshotMjpegChannelGroup.add(ctx.channel());
710 lockCurrentSnapshot.lock();
712 sendMjpegFrame(currentSnapshot, snapshotMjpegChannelGroup);
714 lockCurrentSnapshot.unlock();
716 streamingSnapshotMjpeg = true;
717 startSnapshotPolling();
720 snapshotMjpegChannelGroup.remove(ctx.channel());
721 autoSnapshotMjpegChannelGroup.remove(ctx.channel());
722 if (streamingSnapshotMjpeg && snapshotMjpegChannelGroup.isEmpty()) {
723 streamingSnapshotMjpeg = false;
724 stopSnapshotPolling();
725 logger.debug("All snapshots.mjpeg streams have stopped.");
726 } else if (streamingAutoFps && autoSnapshotMjpegChannelGroup.isEmpty()) {
727 streamingAutoFps = false;
728 stopSnapshotPolling();
729 logger.debug("All autofps.mjpeg streams have stopped.");
734 // If start is true the CTX is added to the list to stream video to, false stops
736 public void setupMjpegStreaming(boolean start, ChannelHandlerContext ctx) {
738 if (mjpegChannelGroup.isEmpty()) {// first stream being requested.
739 mjpegChannelGroup.add(ctx.channel());
740 if (mjpegUri.isEmpty() || mjpegUri.equals("ffmpeg")) {
741 sendMjpegFirstPacket(ctx);
742 setupFfmpegFormat(FFmpegFormat.MJPEG);
745 // fix Dahua reboots when refreshing a mjpeg stream.
746 TimeUnit.MILLISECONDS.sleep(500);
747 } catch (InterruptedException e) {
749 sendHttpGET(mjpegUri);
751 } else if (ffmpegMjpeg != null) {// not first stream and we will use ffmpeg
752 sendMjpegFirstPacket(ctx);
753 mjpegChannelGroup.add(ctx.channel());
754 } else {// not first stream and camera supplies the mjpeg source.
755 ctx.channel().writeAndFlush(firstStreamedMsg);
756 mjpegChannelGroup.add(ctx.channel());
759 mjpegChannelGroup.remove(ctx.channel());
760 if (mjpegChannelGroup.isEmpty()) {
761 logger.debug("All ipcamera.mjpeg streams have stopped.");
762 if (mjpegUri.equals("ffmpeg")) {
763 if (ffmpegMjpeg != null) {
764 ffmpegMjpeg.stopConverting();
766 } else if (!mjpegUri.isEmpty()) {
767 closeChannel(getTinyUrl(mjpegUri));
769 if (ffmpegMjpeg != null) {
770 ffmpegMjpeg.stopConverting();
777 void closeChannel(String url) {
778 ChannelTracking channelTracking = channelTrackingMap.get(url);
779 if (channelTracking != null) {
780 if (channelTracking.getChannel().isOpen()) {
781 channelTracking.getChannel().close();
788 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
789 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
792 void cleanChannels() {
793 for (Channel channel : openChannels) {
794 boolean oldChannel = true;
795 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
796 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
797 channelTrackingMap.remove(channelTracking.getRequestUrl());
799 if (channelTracking.getChannel() == channel) {
800 logger.trace("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
810 public void storeHttpReply(String url, String content) {
811 ChannelTracking channelTracking = channelTrackingMap.get(url);
812 if (channelTracking != null) {
813 channelTracking.setReply(content);
817 // sends direct to ctx so can be either snapshots.mjpeg or normal mjpeg stream
818 public void sendMjpegFirstPacket(ChannelHandlerContext ctx) {
819 final String boundary = "thisMjpegStream";
820 String contentType = "multipart/x-mixed-replace; boundary=" + boundary;
821 HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
822 response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
823 response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
824 response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
825 response.headers().add("Access-Control-Allow-Origin", "*");
826 response.headers().add("Access-Control-Expose-Headers", "*");
827 ctx.channel().writeAndFlush(response);
830 public void sendMjpegFrame(byte[] jpg, ChannelGroup channelGroup) {
831 final String boundary = "thisMjpegStream";
832 ByteBuf imageByteBuf = Unpooled.copiedBuffer(jpg);
833 int length = imageByteBuf.readableBytes();
834 String header = "--" + boundary + "\r\n" + "content-type: image/jpeg" + "\r\n" + "content-length: " + length
836 ByteBuf headerBbuf = Unpooled.copiedBuffer(header, 0, header.length(), StandardCharsets.UTF_8);
837 ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
838 streamToGroup(headerBbuf, channelGroup, false);
839 streamToGroup(imageByteBuf, channelGroup, false);
840 streamToGroup(footerBbuf, channelGroup, true);
843 public void streamToGroup(Object msg, ChannelGroup channelGroup, boolean flush) {
844 channelGroup.write(msg);
846 channelGroup.flush();
850 private void storeSnapshots() {
852 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
853 lockCurrentSnapshot.lock();
855 for (byte[] foo : fifoSnapshotBuffer) {
856 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
859 OutputStream fos = new FileOutputStream(file);
862 } catch (FileNotFoundException e) {
863 logger.warn("FileNotFoundException {}", e.getMessage());
864 } catch (IOException e) {
865 logger.warn("IOException {}", e.getMessage());
869 lockCurrentSnapshot.unlock();
873 public void setupFfmpegFormat(FFmpegFormat format) {
874 String inputOptions = cameraConfig.getFfmpegInputOptions();
875 if (cameraConfig.getFfmpegOutput().isEmpty()) {
876 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
879 if (rtspUri.isEmpty()) {
880 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
883 if (cameraConfig.getFfmpegLocation().isEmpty()) {
884 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
887 if (rtspUri.toLowerCase().contains("rtsp")) {
888 if (inputOptions.isEmpty()) {
889 inputOptions = "-rtsp_transport tcp";
891 inputOptions = inputOptions + " -rtsp_transport tcp";
895 // Make sure the folder exists, if not create it.
896 new File(cameraConfig.getFfmpegOutput()).mkdirs();
899 if (ffmpegHLS == null) {
900 if (!inputOptions.isEmpty()) {
901 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
902 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
903 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
904 cameraConfig.getUser(), cameraConfig.getPassword());
906 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
907 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
908 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
909 cameraConfig.getPassword());
912 if (ffmpegHLS != null) {
913 ffmpegHLS.startConverting();
917 if (cameraConfig.getGifPreroll() > 0) {
918 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
919 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
920 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
921 + cameraConfig.getGifOutOptions(),
922 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
923 cameraConfig.getPassword());
925 if (!inputOptions.isEmpty()) {
926 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
928 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
930 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
931 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
932 cameraConfig.getUser(), cameraConfig.getPassword());
934 if (cameraConfig.getGifPreroll() > 0) {
937 if (ffmpegGIF != null) {
938 ffmpegGIF.startConverting();
939 if (gifHistory.isEmpty()) {
940 gifHistory = gifFilename;
941 } else if (!gifFilename.equals("ipcamera")) {
942 gifHistory = gifFilename + "," + gifHistory;
943 if (gifHistoryLength > 49) {
944 int endIndex = gifHistory.lastIndexOf(",");
945 gifHistory = gifHistory.substring(0, endIndex);
948 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
952 if (!inputOptions.isEmpty()) {
953 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
955 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
957 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
958 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
959 cameraConfig.getUser(), cameraConfig.getPassword());
960 ffmpegRecord.startConverting();
961 if (mp4History.isEmpty()) {
962 mp4History = mp4Filename;
963 } else if (!mp4Filename.equals("ipcamera")) {
964 mp4History = mp4Filename + "," + mp4History;
965 if (mp4HistoryLength > 49) {
966 int endIndex = mp4History.lastIndexOf(",");
967 mp4History = mp4History.substring(0, endIndex);
970 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
973 if (ffmpegRtspHelper != null) {
974 ffmpegRtspHelper.stopConverting();
975 if (!audioAlarmEnabled && !motionAlarmEnabled) {
979 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
980 String outputOptions = "-f null -";
981 String filterOptions = "";
982 if (!audioAlarmEnabled) {
983 filterOptions = "-an";
985 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
987 if (!motionAlarmEnabled && !ffmpegSnapshotGeneration) {
988 filterOptions = filterOptions.concat(" -vn");
989 } else if (motionAlarmEnabled) {
990 filterOptions = filterOptions
991 .concat(" -vf select='gte(scene," + motionThreshold + ")',metadata=print");
993 if (!cameraConfig.getUser().isEmpty()) {
994 filterOptions += " ";// add space as the Framework does not allow spaces at start of config.
996 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
997 filterOptions + cameraConfig.getMotionOptions(), outputOptions, cameraConfig.getUser(),
998 cameraConfig.getPassword());
999 ffmpegRtspHelper.startConverting();
1002 if (ffmpegMjpeg == null) {
1003 if (inputOptions.isEmpty()) {
1004 inputOptions = "-hide_banner -loglevel warning";
1006 inputOptions = inputOptions + " -hide_banner -loglevel warning";
1008 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1009 cameraConfig.getMjpegOptions(),
1010 "http://127.0.0.1:" + cameraConfig.getServerPort() + "/ipcamera.jpg",
1011 cameraConfig.getUser(), cameraConfig.getPassword());
1013 if (ffmpegMjpeg != null) {
1014 ffmpegMjpeg.startConverting();
1018 // if mjpeg stream you can use ffmpeg -i input.h264 -codec:v copy -bsf:v mjpeg2jpeg output%03d.jpg
1019 if (ffmpegSnapshot == null) {
1020 if (inputOptions.isEmpty()) {
1022 inputOptions = "-threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1024 inputOptions = inputOptions + " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1026 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1027 "-an -vsync vfr -update 1",
1028 "http://127.0.0.1:" + cameraConfig.getServerPort() + "/snapshot.jpg",
1029 cameraConfig.getUser(), cameraConfig.getPassword());
1031 if (ffmpegSnapshot != null) {
1032 ffmpegSnapshot.startConverting();
1038 public void noMotionDetected(String thisAlarmsChannel) {
1039 setChannelState(thisAlarmsChannel, OnOffType.OFF);
1040 firstMotionAlarm = false;
1041 motionAlarmUpdateSnapshot = false;
1042 motionDetected = false;
1043 if (streamingAutoFps) {
1044 stopSnapshotPolling();
1045 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1046 stopSnapshotPolling();
1051 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
1052 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
1053 * tampering with the camera.
1055 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
1056 updateState(thisAlarmsChannel, state);
1059 public void motionDetected(String thisAlarmsChannel) {
1060 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
1061 updateState(thisAlarmsChannel, OnOffType.ON);
1062 motionDetected = true;
1063 if (streamingAutoFps) {
1064 startSnapshotPolling();
1066 if (cameraConfig.getUpdateImageWhen().contains("2")) {
1067 if (!firstMotionAlarm) {
1068 if (!snapshotUri.isEmpty()) {
1069 sendHttpGET(snapshotUri);
1071 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1073 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1074 if (!snapshotPolling) {
1075 startSnapshotPolling();
1077 firstMotionAlarm = true;
1078 motionAlarmUpdateSnapshot = true;
1082 public void audioDetected() {
1083 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1084 if (cameraConfig.getUpdateImageWhen().contains("3")) {
1085 if (!firstAudioAlarm) {
1086 if (!snapshotUri.isEmpty()) {
1087 sendHttpGET(snapshotUri);
1089 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1091 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1092 firstAudioAlarm = true;
1093 audioAlarmUpdateSnapshot = true;
1097 public void noAudioDetected() {
1098 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1099 firstAudioAlarm = false;
1100 audioAlarmUpdateSnapshot = false;
1103 public void recordMp4(String filename, int seconds) {
1104 mp4Filename = filename;
1105 mp4RecordTime = seconds;
1106 setupFfmpegFormat(FFmpegFormat.RECORD);
1107 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1110 public void recordGif(String filename, int seconds) {
1111 gifFilename = filename;
1112 gifRecordTime = seconds;
1113 if (cameraConfig.getGifPreroll() > 0) {
1114 snapCount = seconds;
1116 setupFfmpegFormat(FFmpegFormat.GIF);
1118 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1121 public String returnValueFromString(String rawString, String searchedString) {
1123 int index = rawString.indexOf(searchedString);
1124 if (index != -1) // -1 means "not found"
1126 result = rawString.substring(index + searchedString.length(), rawString.length());
1127 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1129 return result; // Did not find a carriage return.
1131 return result.substring(0, index);
1134 return ""; // Did not find the String we were searching for
1137 private void sendPTZRequest() {
1138 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1142 public void handleCommand(ChannelUID channelUID, Command command) {
1143 if (command instanceof RefreshType) {
1144 switch (channelUID.getId()) {
1146 if (onvifCamera.supportsPTZ()) {
1147 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1151 if (onvifCamera.supportsPTZ()) {
1152 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1156 if (onvifCamera.supportsPTZ()) {
1157 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1160 case CHANNEL_GOTO_PRESET:
1161 if (onvifCamera.supportsPTZ()) {
1162 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1166 } // caution "REFRESH" can still progress to brand Handlers below the else.
1168 switch (channelUID.getId()) {
1169 case CHANNEL_MP4_HISTORY_LENGTH:
1170 if (DecimalType.ZERO.equals(command)) {
1171 mp4HistoryLength = 0;
1173 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1176 case CHANNEL_GIF_HISTORY_LENGTH:
1177 if (DecimalType.ZERO.equals(command)) {
1178 gifHistoryLength = 0;
1180 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1183 case CHANNEL_FFMPEG_MOTION_CONTROL:
1184 if (OnOffType.ON.equals(command)) {
1185 motionAlarmEnabled = true;
1186 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1187 motionAlarmEnabled = false;
1188 noMotionDetected(CHANNEL_MOTION_ALARM);
1190 motionAlarmEnabled = true;
1191 motionThreshold = Double.valueOf(command.toString());
1192 motionThreshold = motionThreshold / 10000;
1194 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1196 case CHANNEL_START_STREAM:
1197 if (OnOffType.ON.equals(command)) {
1198 setupFfmpegFormat(FFmpegFormat.HLS);
1199 if (ffmpegHLS != null) {
1200 ffmpegHLS.setKeepAlive(-1);// will keep running till manually stopped.
1203 if (ffmpegHLS != null) {
1204 ffmpegHLS.setKeepAlive(1);
1208 case CHANNEL_EXTERNAL_MOTION:
1209 if (OnOffType.ON.equals(command)) {
1210 motionDetected(CHANNEL_EXTERNAL_MOTION);
1212 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1215 case CHANNEL_GOTO_PRESET:
1216 if (onvifCamera.supportsPTZ()) {
1217 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1220 case CHANNEL_POLL_IMAGE:
1221 if (OnOffType.ON.equals(command)) {
1222 if (snapshotUri.isEmpty()) {
1223 ffmpegSnapshotGeneration = true;
1224 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1225 updateImageChannel = false;
1227 updateImageChannel = true;
1228 sendHttpGET(snapshotUri);// Allows this to change Image FPS on demand
1231 if (ffmpegSnapshot != null) {
1232 ffmpegSnapshot.stopConverting();
1233 ffmpegSnapshotGeneration = false;
1235 updateImageChannel = false;
1239 if (onvifCamera.supportsPTZ()) {
1240 if (command instanceof IncreaseDecreaseType) {
1241 if (command == IncreaseDecreaseType.INCREASE) {
1242 if (cameraConfig.getPtzContinuous()) {
1243 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1245 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1248 if (cameraConfig.getPtzContinuous()) {
1249 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1251 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1255 } else if (OnOffType.OFF.equals(command)) {
1256 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1259 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1260 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1264 if (onvifCamera.supportsPTZ()) {
1265 if (command instanceof IncreaseDecreaseType) {
1266 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1267 if (cameraConfig.getPtzContinuous()) {
1268 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1270 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1273 if (cameraConfig.getPtzContinuous()) {
1274 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1276 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1280 } else if (OnOffType.OFF.equals(command)) {
1281 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1284 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1285 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1289 if (onvifCamera.supportsPTZ()) {
1290 if (command instanceof IncreaseDecreaseType) {
1291 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1292 if (cameraConfig.getPtzContinuous()) {
1293 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1295 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1298 if (cameraConfig.getPtzContinuous()) {
1299 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1301 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1305 } else if (OnOffType.OFF.equals(command)) {
1306 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1309 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1310 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1315 // commands and refresh now get passed to brand handlers
1316 switch (thing.getThingTypeUID().getId()) {
1318 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1319 amcrestHandler.handleCommand(channelUID, command);
1320 if (lowPriorityRequests.isEmpty()) {
1321 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1325 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1326 dahuaHandler.handleCommand(channelUID, command);
1327 if (lowPriorityRequests.isEmpty()) {
1328 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1331 case DOORBIRD_THING:
1332 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1333 doorBirdHandler.handleCommand(channelUID, command);
1334 if (lowPriorityRequests.isEmpty()) {
1335 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1338 case HIKVISION_THING:
1339 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1340 hikvisionHandler.handleCommand(channelUID, command);
1341 if (lowPriorityRequests.isEmpty()) {
1342 lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1346 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1347 cameraConfig.getPassword());
1348 foscamHandler.handleCommand(channelUID, command);
1349 if (lowPriorityRequests.isEmpty()) {
1350 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1354 InstarHandler instarHandler = new InstarHandler(getHandle());
1355 instarHandler.handleCommand(channelUID, command);
1356 if (lowPriorityRequests.isEmpty()) {
1357 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1361 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1362 defaultHandler.handleCommand(channelUID, command);
1363 if (lowPriorityRequests.isEmpty()) {
1364 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1370 public void setChannelState(String channelToUpdate, State valueOf) {
1371 updateState(channelToUpdate, valueOf);
1374 void bringCameraOnline() {
1376 updateStatus(ThingStatus.ONLINE);
1377 groupTracker.listOfOnlineCameraHandlers.add(this);
1378 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1379 if (cameraConnectionJob != null) {
1380 cameraConnectionJob.cancel(false);
1383 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1384 snapshotPolling = true;
1385 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 1000, cameraConfig.getPollTime(),
1386 TimeUnit.MILLISECONDS);
1389 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1391 if (!rtspUri.isEmpty()) {
1392 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1394 if (updateImageChannel) {
1395 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1397 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1399 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1400 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1401 handle.cameraOnline(getThing().getUID().getId());
1406 void snapshotIsFfmpeg() {
1407 bringCameraOnline();
1408 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1410 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1411 if (!rtspUri.isEmpty()) {
1412 updateImageChannel = false;
1413 ffmpegSnapshotGeneration = true;
1414 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1415 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1417 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1421 void pollingCameraConnection() {
1422 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1423 if (rtspUri.isEmpty()) {
1424 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1426 if (snapshotUri.isEmpty() || snapshotUri.equals("ffmpeg")) {
1429 sendHttpRequest("GET", snapshotUri, null);
1433 if (!onvifCamera.isConnected()) {
1434 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1435 cameraConfig.getOnvifPort());
1436 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1438 if (snapshotUri.equals("ffmpeg")) {
1440 } else if (!snapshotUri.isEmpty()) {
1441 sendHttpRequest("GET", snapshotUri, null);
1442 } else if (!rtspUri.isEmpty()) {
1445 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1446 "Camera failed to report a valid Snaphot and/or RTSP URL. See readme on how to use the SNAPSHOT_URL_OVERRIDE feature.");
1450 public void cameraConfigError(String reason) {
1451 // wont try to reconnect again due to a config error being the cause.
1452 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1456 public void cameraCommunicationError(String reason) {
1457 // will try to reconnect again as camera may be rebooting.
1458 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1459 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1460 resetAndRetryConnecting();
1464 boolean streamIsStopped(String url) {
1465 ChannelTracking channelTracking = channelTrackingMap.get(url);
1466 if (channelTracking != null) {
1467 if (channelTracking.getChannel().isOpen()) {
1468 return false; // stream is running.
1471 return true; // Stream stopped or never started.
1474 void snapshotRunnable() {
1475 // Snapshot should be first to keep consistent time between shots
1476 sendHttpGET(snapshotUri);
1477 if (snapCount > 0) {
1478 if (--snapCount == 0) {
1479 setupFfmpegFormat(FFmpegFormat.GIF);
1484 public void stopSnapshotPolling() {
1485 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1486 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1487 snapshotPolling = false;
1488 if (snapshotJob != null) {
1489 snapshotJob.cancel(true);
1491 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1492 snapshotPolling = false;
1493 if (snapshotJob != null) {
1494 snapshotJob.cancel(true);
1499 public void startSnapshotPolling() {
1500 if (snapshotPolling || ffmpegSnapshotGeneration) {
1501 return; // Already polling or creating with FFmpeg from RTSP
1503 if (streamingSnapshotMjpeg || streamingAutoFps) {
1504 snapshotPolling = true;
1505 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1506 TimeUnit.MILLISECONDS);
1507 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1508 snapshotPolling = true;
1509 snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1510 TimeUnit.MILLISECONDS);
1514 // runs every 8 seconds due to mjpeg streams not staying open unless they update this often.
1515 void pollCameraRunnable() {
1516 // Snapshot should be first to keep consistent time between shots
1517 if (!snapshotUri.isEmpty()) {
1518 if (updateImageChannel) {
1519 sendHttpGET(snapshotUri);
1522 if (streamingAutoFps) {
1523 updateAutoFps = true;
1524 if (!snapshotPolling && !ffmpegSnapshotGeneration) {
1525 // Dont need to poll if creating from RTSP stream with FFmpeg or we are polling at full rate already.
1526 sendHttpGET(snapshotUri);
1529 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1530 if (!lowPriorityRequests.isEmpty()) {
1531 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1532 lowPriorityCounter = 0;
1534 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1536 // what needs to be done every poll//
1537 switch (thing.getThingTypeUID().getId()) {
1541 if (!onvifCamera.isConnected()) {
1542 onvifCamera.connect(true);
1546 noMotionDetected(CHANNEL_MOTION_ALARM);
1547 noMotionDetected(CHANNEL_PIR_ALARM);
1550 case HIKVISION_THING:
1551 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1552 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1553 cameraConfig.getIp());
1554 sendHttpGET("/ISAPI/Event/notification/alertStream");
1558 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1559 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1562 // Check for alarms, channel for NVRs appears not to work at filtering.
1563 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1564 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1565 cameraConfig.getIp());
1566 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1569 case DOORBIRD_THING:
1570 // Check for alarms, channel for NVRs appears not to work at filtering.
1571 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1572 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1573 cameraConfig.getIp());
1574 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1578 if (ffmpegHLS != null) {
1579 ffmpegHLS.checkKeepAlive();
1581 if (openChannels.size() > 18) {
1582 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1588 public void initialize() {
1589 cameraConfig = getConfigAs(CameraConfig.class);
1590 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1591 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1592 rtspUri = cameraConfig.getFfmpegInput();
1594 if (cameraConfig.getServerPort() < 1) {
1596 "The Server Port is not set to a valid number which disables a lot of binding features. See readme for more info.");
1597 } else if (cameraConfig.getServerPort() < 1025) {
1598 logger.warn("The Server Port is <= 1024 and may cause permission errors under Linux, try a higher number.");
1601 // Known cameras will connect quicker if we skip ONVIF questions.
1602 switch (thing.getThingTypeUID().getId()) {
1605 if (mjpegUri.isEmpty()) {
1606 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1608 if (snapshotUri.isEmpty()) {
1609 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1612 case DOORBIRD_THING:
1613 if (mjpegUri.isEmpty()) {
1614 mjpegUri = "/bha-api/video.cgi";
1616 if (snapshotUri.isEmpty()) {
1617 snapshotUri = "/bha-api/image.cgi";
1621 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1622 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1623 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1624 if (mjpegUri.isEmpty()) {
1625 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1626 + cameraConfig.getPassword();
1628 if (snapshotUri.isEmpty()) {
1629 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1630 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1633 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1634 if (mjpegUri.isEmpty()) {
1635 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1637 if (snapshotUri.isEmpty()) {
1638 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1642 if (snapshotUri.isEmpty()) {
1643 snapshotUri = "/tmpfs/snap.jpg";
1645 if (mjpegUri.isEmpty()) {
1646 mjpegUri = "/mjpegstream.cgi?-chn=12";
1651 // Onvif and Instar event handling needs the host IP and the server started.
1652 if (cameraConfig.getServerPort() > 0) {
1653 startStreamServer();
1656 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1657 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1658 cameraConfig.getUser(), cameraConfig.getPassword());
1659 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1660 // Only use ONVIF events if it is not an API camera.
1661 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1664 // for poll times above 9 seconds don't display a warning about the Image channel.
1665 if (9000 <= cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1667 "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.");
1669 // Waiting 3 seconds for ONVIF to discover the urls before running.
1670 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 30, TimeUnit.SECONDS);
1673 // What the camera needs to re-connect if the initialize() is not called.
1674 private void resetAndRetryConnecting() {
1680 public void dispose() {
1682 snapshotPolling = false;
1683 onvifCamera.disconnect();
1684 if (pollCameraJob != null) {
1685 pollCameraJob.cancel(true);
1686 pollCameraJob = null;
1688 if (snapshotJob != null) {
1689 snapshotJob.cancel(true);
1692 if (cameraConnectionJob != null) {
1693 cameraConnectionJob.cancel(true);
1694 cameraConnectionJob = null;
1696 threadPool.shutdown();
1697 threadPool = Executors.newScheduledThreadPool(4);
1699 groupTracker.listOfOnlineCameraHandlers.remove(this);
1700 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1701 // inform all group handlers that this camera has gone offline
1702 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1703 handle.cameraOffline(this);
1705 basicAuth = ""; // clear out stored Password hash
1706 useDigestAuth = false;
1708 openChannels.close();
1710 if (ffmpegHLS != null) {
1711 ffmpegHLS.stopConverting();
1714 if (ffmpegRecord != null) {
1715 ffmpegRecord.stopConverting();
1716 ffmpegRecord = null;
1718 if (ffmpegGIF != null) {
1719 ffmpegGIF.stopConverting();
1722 if (ffmpegRtspHelper != null) {
1723 ffmpegRtspHelper.stopConverting();
1724 ffmpegRtspHelper = null;
1726 if (ffmpegMjpeg != null) {
1727 ffmpegMjpeg.stopConverting();
1730 if (ffmpegSnapshot != null) {
1731 ffmpegSnapshot.stopConverting();
1732 ffmpegSnapshot = null;
1734 channelTrackingMap.clear();
1737 public void setStreamServerHandler(StreamServerHandler streamServerHandler2) {
1738 streamServerHandler = streamServerHandler2;
1741 public String getWhiteList() {
1742 return cameraConfig.getIpWhitelist();
1746 public Collection<Class<? extends ThingHandlerService>> getServices() {
1747 return Collections.singleton(IpCameraActions.class);