2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.ipcamera.internal.handler;
15 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
18 import java.io.FileNotFoundException;
19 import java.io.FileOutputStream;
20 import java.io.IOException;
21 import java.io.OutputStream;
22 import java.net.InetSocketAddress;
23 import java.net.MalformedURLException;
25 import java.nio.charset.StandardCharsets;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.LinkedList;
30 import java.util.List;
32 import java.util.concurrent.ConcurrentHashMap;
33 import java.util.concurrent.Executors;
34 import java.util.concurrent.Future;
35 import java.util.concurrent.ScheduledExecutorService;
36 import java.util.concurrent.ScheduledFuture;
37 import java.util.concurrent.TimeUnit;
38 import java.util.concurrent.locks.ReentrantLock;
40 import org.eclipse.jdt.annotation.NonNullByDefault;
41 import org.eclipse.jdt.annotation.Nullable;
42 import org.openhab.binding.ipcamera.internal.AmcrestHandler;
43 import org.openhab.binding.ipcamera.internal.CameraConfig;
44 import org.openhab.binding.ipcamera.internal.ChannelTracking;
45 import org.openhab.binding.ipcamera.internal.DahuaHandler;
46 import org.openhab.binding.ipcamera.internal.DoorBirdHandler;
47 import org.openhab.binding.ipcamera.internal.Ffmpeg;
48 import org.openhab.binding.ipcamera.internal.FoscamHandler;
49 import org.openhab.binding.ipcamera.internal.GroupTracker;
50 import org.openhab.binding.ipcamera.internal.Helper;
51 import org.openhab.binding.ipcamera.internal.HikvisionHandler;
52 import org.openhab.binding.ipcamera.internal.HttpOnlyHandler;
53 import org.openhab.binding.ipcamera.internal.InstarHandler;
54 import org.openhab.binding.ipcamera.internal.IpCameraActions;
55 import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
56 import org.openhab.binding.ipcamera.internal.IpCameraDynamicStateDescriptionProvider;
57 import org.openhab.binding.ipcamera.internal.MyNettyAuthHandler;
58 import org.openhab.binding.ipcamera.internal.StreamServerHandler;
59 import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection;
60 import org.openhab.core.OpenHAB;
61 import org.openhab.core.library.types.DecimalType;
62 import org.openhab.core.library.types.IncreaseDecreaseType;
63 import org.openhab.core.library.types.OnOffType;
64 import org.openhab.core.library.types.PercentType;
65 import org.openhab.core.library.types.RawType;
66 import org.openhab.core.library.types.StringType;
67 import org.openhab.core.thing.ChannelUID;
68 import org.openhab.core.thing.Thing;
69 import org.openhab.core.thing.ThingStatus;
70 import org.openhab.core.thing.ThingStatusDetail;
71 import org.openhab.core.thing.binding.BaseThingHandler;
72 import org.openhab.core.thing.binding.ThingHandlerService;
73 import org.openhab.core.types.Command;
74 import org.openhab.core.types.RefreshType;
75 import org.openhab.core.types.State;
76 import org.slf4j.Logger;
77 import org.slf4j.LoggerFactory;
79 import io.netty.bootstrap.Bootstrap;
80 import io.netty.bootstrap.ServerBootstrap;
81 import io.netty.buffer.ByteBuf;
82 import io.netty.buffer.Unpooled;
83 import io.netty.channel.Channel;
84 import io.netty.channel.ChannelDuplexHandler;
85 import io.netty.channel.ChannelFuture;
86 import io.netty.channel.ChannelFutureListener;
87 import io.netty.channel.ChannelHandlerContext;
88 import io.netty.channel.ChannelInitializer;
89 import io.netty.channel.ChannelOption;
90 import io.netty.channel.EventLoopGroup;
91 import io.netty.channel.group.ChannelGroup;
92 import io.netty.channel.group.DefaultChannelGroup;
93 import io.netty.channel.nio.NioEventLoopGroup;
94 import io.netty.channel.socket.SocketChannel;
95 import io.netty.channel.socket.nio.NioServerSocketChannel;
96 import io.netty.channel.socket.nio.NioSocketChannel;
97 import io.netty.handler.codec.base64.Base64;
98 import io.netty.handler.codec.http.DefaultFullHttpRequest;
99 import io.netty.handler.codec.http.DefaultHttpResponse;
100 import io.netty.handler.codec.http.FullHttpRequest;
101 import io.netty.handler.codec.http.HttpClientCodec;
102 import io.netty.handler.codec.http.HttpContent;
103 import io.netty.handler.codec.http.HttpHeaderNames;
104 import io.netty.handler.codec.http.HttpHeaderValues;
105 import io.netty.handler.codec.http.HttpMessage;
106 import io.netty.handler.codec.http.HttpMethod;
107 import io.netty.handler.codec.http.HttpResponse;
108 import io.netty.handler.codec.http.HttpResponseStatus;
109 import io.netty.handler.codec.http.HttpServerCodec;
110 import io.netty.handler.codec.http.HttpVersion;
111 import io.netty.handler.codec.http.LastHttpContent;
112 import io.netty.handler.stream.ChunkedWriteHandler;
113 import io.netty.handler.timeout.IdleState;
114 import io.netty.handler.timeout.IdleStateEvent;
115 import io.netty.handler.timeout.IdleStateHandler;
116 import io.netty.util.CharsetUtil;
117 import io.netty.util.ReferenceCountUtil;
118 import io.netty.util.concurrent.GlobalEventExecutor;
121 * The {@link IpCameraHandler} is responsible for handling commands, which are
122 * sent to one of the channels.
124 * @author Matthew Skinner - Initial contribution
128 public class IpCameraHandler extends BaseThingHandler {
129 public final Logger logger = LoggerFactory.getLogger(getClass());
130 public final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
131 private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(4);
132 private GroupTracker groupTracker;
133 public CameraConfig cameraConfig = new CameraConfig();
135 // ChannelGroup is thread safe
136 public final ChannelGroup mjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
137 private final ChannelGroup snapshotMjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
138 private final ChannelGroup autoSnapshotMjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
139 public final ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
140 public @Nullable Ffmpeg ffmpegHLS = null;
141 public @Nullable Ffmpeg ffmpegRecord = null;
142 public @Nullable Ffmpeg ffmpegGIF = null;
143 public @Nullable Ffmpeg ffmpegRtspHelper = null;
144 public @Nullable Ffmpeg ffmpegMjpeg = null;
145 public @Nullable Ffmpeg ffmpegSnapshot = null;
146 public boolean streamingAutoFps = false;
147 public boolean motionDetected = false;
149 private @Nullable ScheduledFuture<?> cameraConnectionJob = null;
150 private @Nullable ScheduledFuture<?> pollCameraJob = null;
151 private @Nullable ScheduledFuture<?> snapshotJob = null;
152 private @Nullable Bootstrap mainBootstrap;
153 private @Nullable ServerBootstrap serverBootstrap;
155 private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup();
156 private EventLoopGroup serversLoopGroup = new NioEventLoopGroup();
157 private FullHttpRequest putRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
159 private String gifFilename = "ipcamera";
160 private String gifHistory = "";
161 private String mp4History = "";
162 public int gifHistoryLength;
163 public int mp4HistoryLength;
164 private String mp4Filename = "ipcamera";
165 private int mp4RecordTime;
166 private int gifRecordTime = 5;
167 private LinkedList<byte[]> fifoSnapshotBuffer = new LinkedList<byte[]>();
168 private int snapCount;
169 private boolean updateImageChannel = false;
170 private boolean updateAutoFps = false;
171 private byte lowPriorityCounter = 0;
172 public String hostIp;
173 public Map<String, ChannelTracking> channelTrackingMap = new ConcurrentHashMap<>();
174 public List<String> lowPriorityRequests = new ArrayList<>(0);
176 // basicAuth MUST remain private as it holds the cameraConfig.getPassword()
177 private String basicAuth = "";
178 public boolean useBasicAuth = false;
179 public boolean useDigestAuth = false;
180 public String snapshotUri = "";
181 public String mjpegUri = "";
182 private @Nullable ChannelFuture serverFuture = null;
183 private Object firstStreamedMsg = new Object();
184 public byte[] currentSnapshot = new byte[] { (byte) 0x00 };
185 public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
186 public String rtspUri = "";
187 public boolean audioAlarmUpdateSnapshot = false;
188 private boolean motionAlarmUpdateSnapshot = false;
189 private boolean isOnline = false; // Used so only 1 error is logged when a network issue occurs.
190 private boolean firstAudioAlarm = false;
191 private boolean firstMotionAlarm = false;
192 public Double motionThreshold = 0.0016;
193 public int audioThreshold = 35;
194 @SuppressWarnings("unused")
195 private @Nullable StreamServerHandler streamServerHandler;
196 private boolean streamingSnapshotMjpeg = false;
197 public boolean motionAlarmEnabled = false;
198 public boolean audioAlarmEnabled = false;
199 public boolean ffmpegSnapshotGeneration = false;
200 public boolean snapshotPolling = false;
201 public OnvifConnection onvifCamera = new OnvifConnection(this, "", "", "");
203 // These methods handle the response from all camera brands, nothing specific to 1 brand.
204 private class CommonCameraHandler extends ChannelDuplexHandler {
205 private int bytesToRecieve = 0;
206 private int bytesAlreadyRecieved = 0;
207 private byte[] incomingJpeg = new byte[0];
208 private String incomingMessage = "";
209 private String contentType = "empty";
210 private String boundary = "";
211 private Object reply = new Object();
212 private String requestUrl = "";
213 private boolean closeConnection = true;
214 private boolean isChunked = false;
216 public void setURL(String url) {
221 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
222 if (msg == null || ctx == null) {
226 if (msg instanceof HttpResponse) {
227 HttpResponse response = (HttpResponse) msg;
228 if (response.status().code() != 401) {
229 if (!response.headers().isEmpty()) {
230 for (String name : response.headers().names()) {
231 // Some cameras use first letter uppercase and others dont.
232 switch (name.toLowerCase()) { // Possible localization issues doing this
234 contentType = response.headers().getAsString(name);
236 case "content-length":
237 bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
240 if (response.headers().getAsString(name).contains("keep-alive")) {
241 closeConnection = false;
244 case "transfer-encoding":
245 if (response.headers().getAsString(name).contains("chunked")) {
251 if (contentType.contains("multipart")) {
252 closeConnection = false;
253 if (mjpegUri.equals(requestUrl)) {
254 if (msg instanceof HttpMessage) {
255 // very start of stream only
256 ReferenceCountUtil.retain(msg, 1);
257 firstStreamedMsg = msg;
258 streamToGroup(firstStreamedMsg, mjpegChannelGroup, true);
261 boundary = Helper.searchString(contentType, "boundary=");
263 } else if (contentType.contains("image/jp")) {
264 if (bytesToRecieve == 0) {
265 bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
266 logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
268 incomingJpeg = new byte[bytesToRecieve];
273 if (msg instanceof HttpContent) {
274 if (mjpegUri.equals(requestUrl)) {
275 // multiple MJPEG stream packets come back as this.
276 ReferenceCountUtil.retain(msg, 1);
277 streamToGroup(msg, mjpegChannelGroup, true);
279 HttpContent content = (HttpContent) msg;
280 // Found some cameras use Content-Type: image/jpg instead of image/jpeg
281 if (contentType.contains("image/jp")) {
282 for (int i = 0; i < content.content().capacity(); i++) {
283 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
285 if (content instanceof LastHttpContent) {
286 processSnapshot(incomingJpeg);
287 // testing next line and if works need to do a full cleanup of this function.
288 closeConnection = true;
289 if (closeConnection) {
293 bytesAlreadyRecieved = 0;
296 } else { // incomingMessage that is not an IMAGE
297 if (incomingMessage.isEmpty()) {
298 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
300 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
302 bytesAlreadyRecieved = incomingMessage.length();
303 if (content instanceof LastHttpContent) {
304 // If it is not an image send it on to the next handler//
305 if (bytesAlreadyRecieved != 0) {
306 reply = incomingMessage;
307 super.channelRead(ctx, reply);
310 // Alarm Streams never have a LastHttpContent as they always stay open//
311 else if (contentType.contains("multipart")) {
312 int beginIndex, endIndex;
313 if (bytesToRecieve == 0) {
314 beginIndex = incomingMessage.indexOf("Content-Length:");
315 if (beginIndex != -1) {
316 endIndex = incomingMessage.indexOf("\r\n", beginIndex);
317 if (endIndex != -1) {
318 bytesToRecieve = Integer.parseInt(
319 incomingMessage.substring(beginIndex + 15, endIndex).strip());
323 // --boundary and headers are not included in the Content-Length value
324 if (bytesAlreadyRecieved > bytesToRecieve) {
325 // Check if message has a second --boundary
326 endIndex = incomingMessage.indexOf("--" + boundary, bytesToRecieve);
327 if (endIndex == -1) {
328 reply = incomingMessage;
329 incomingMessage = "";
331 bytesAlreadyRecieved = 0;
333 reply = incomingMessage.substring(0, endIndex);
334 incomingMessage = incomingMessage.substring(endIndex, incomingMessage.length());
335 bytesToRecieve = 0;// Triggers search next time for Content-Length:
336 bytesAlreadyRecieved = incomingMessage.length() - endIndex;
338 super.channelRead(ctx, reply);
341 // Foscam needs this as will other cameras with chunks//
342 if (isChunked && bytesAlreadyRecieved != 0) {
343 logger.debug("Reply is chunked.");
344 reply = incomingMessage;
345 super.channelRead(ctx, reply);
349 } else { // msg is not HttpContent
350 // Foscam cameras need this
351 if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
352 reply = incomingMessage;
353 logger.debug("Packet back from camera is {}", incomingMessage);
354 super.channelRead(ctx, reply);
358 ReferenceCountUtil.release(msg);
363 public void channelReadComplete(@Nullable ChannelHandlerContext ctx) {
367 public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
371 public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
375 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
376 if (cause == null || ctx == null) {
379 if (cause instanceof ArrayIndexOutOfBoundsException) {
380 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
383 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
390 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
394 if (evt instanceof IdleStateEvent) {
395 IdleStateEvent e = (IdleStateEvent) evt;
396 // If camera does not use the channel for X amount of time it will close.
397 if (e.state() == IdleState.READER_IDLE) {
398 String urlToKeepOpen = "";
399 switch (thing.getThingTypeUID().getId()) {
401 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
403 case HIKVISION_THING:
404 urlToKeepOpen = "/ISAPI/Event/notification/alertStream";
407 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
410 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
411 if (channelTracking != null) {
412 if (channelTracking.getChannel() == ctx.channel()) {
413 return; // don't auto close this as it is for the alarms.
422 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
423 IpCameraDynamicStateDescriptionProvider stateDescriptionProvider) {
425 this.stateDescriptionProvider = stateDescriptionProvider;
426 if (ipAddress != null) {
429 hostIp = Helper.getLocalIpAddress();
431 this.groupTracker = groupTracker;
434 private IpCameraHandler getHandle() {
438 // false clears the stored user/pass hash, true creates the hash
439 public boolean setBasicAuth(boolean useBasic) {
441 logger.debug("Clearing out the stored BASIC auth now.");
444 } else if (!basicAuth.isEmpty()) {
445 // due to camera may have been sent multiple requests before the auth was set, this may trigger falsely.
446 logger.warn("Camera is reporting your username and/or password is wrong.");
449 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
450 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
451 ByteBuf byteBuf = null;
453 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
454 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
456 if (byteBuf != null) {
462 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
467 private String getCorrectUrlFormat(String longUrl) {
468 String temp = longUrl;
471 if (longUrl.isEmpty() || "ffmpeg".equals(longUrl)) {
476 url = new URL(longUrl);
477 int port = url.getPort();
479 if (url.getQuery() == null) {
480 temp = url.getPath();
482 temp = url.getPath() + "?" + url.getQuery();
485 if (url.getQuery() == null) {
486 temp = ":" + url.getPort() + url.getPath();
488 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
491 } catch (MalformedURLException e) {
492 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
497 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
498 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
499 sendHttpRequest("PUT", httpRequestURL, null);
502 public void sendHttpGET(String httpRequestURL) {
503 sendHttpRequest("GET", httpRequestURL, null);
506 public int getPortFromShortenedUrl(String httpRequestURL) {
507 if (httpRequestURL.startsWith(":")) {
508 int end = httpRequestURL.indexOf("/");
509 return Integer.parseInt(httpRequestURL.substring(1, end));
511 return cameraConfig.getPort();
514 public String getTinyUrl(String httpRequestURL) {
515 if (httpRequestURL.startsWith(":")) {
516 int beginIndex = httpRequestURL.indexOf("/");
517 return httpRequestURL.substring(beginIndex);
519 return httpRequestURL;
522 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
523 // The authHandler will generate a digest string and re-send using this same function when needed.
524 @SuppressWarnings("null")
525 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
526 int port = getPortFromShortenedUrl(httpRequestURLFull);
527 String httpRequestURL = getTinyUrl(httpRequestURLFull);
529 if (mainBootstrap == null) {
530 mainBootstrap = new Bootstrap();
531 mainBootstrap.group(mainEventLoopGroup);
532 mainBootstrap.channel(NioSocketChannel.class);
533 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
534 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
535 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
536 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
537 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
538 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
541 public void initChannel(SocketChannel socketChannel) throws Exception {
542 // HIK Alarm stream needs > 9sec idle to stop stream closing
543 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
544 socketChannel.pipeline().addLast(new HttpClientCodec());
545 socketChannel.pipeline().addLast(AUTH_HANDLER,
546 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
547 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
549 switch (thing.getThingTypeUID().getId()) {
551 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
554 socketChannel.pipeline()
555 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
558 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
561 socketChannel.pipeline().addLast(
562 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
564 case HIKVISION_THING:
565 socketChannel.pipeline()
566 .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
569 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
572 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
579 FullHttpRequest request;
580 if (!"PUT".equals(httpMethod) || (useDigestAuth && digestString == null)) {
581 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
582 request.headers().set("Host", cameraConfig.getIp() + ":" + port);
583 request.headers().set("Connection", HttpHeaderValues.KEEP_ALIVE);
585 request = putRequestWithBody;
588 if (!basicAuth.isEmpty()) {
590 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
593 request.headers().set("Authorization", "Basic " + basicAuth);
598 if (digestString != null) {
599 request.headers().set("Authorization", "Digest " + digestString);
603 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
604 .addListener(new ChannelFutureListener() {
607 public void operationComplete(@Nullable ChannelFuture future) {
608 if (future == null) {
611 if (future.isDone() && future.isSuccess()) {
612 Channel ch = future.channel();
613 openChannels.add(ch);
617 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
620 openChannel(ch, httpRequestURL);
621 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
622 commonHandler.setURL(httpRequestURLFull);
623 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
624 authHandler.setURL(httpMethod, httpRequestURL);
626 switch (thing.getThingTypeUID().getId()) {
628 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
629 amcrestHandler.setURL(httpRequestURL);
632 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
633 instarHandler.setURL(httpRequestURL);
636 ch.writeAndFlush(request);
637 } else { // an error occured
638 cameraCommunicationError(
639 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
645 public void processSnapshot(byte[] incommingSnapshot) {
646 lockCurrentSnapshot.lock();
648 currentSnapshot = incommingSnapshot;
649 if (cameraConfig.getGifPreroll() > 0) {
650 fifoSnapshotBuffer.add(incommingSnapshot);
651 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
652 fifoSnapshotBuffer.removeFirst();
656 lockCurrentSnapshot.unlock();
659 if (streamingSnapshotMjpeg) {
660 sendMjpegFrame(incommingSnapshot, snapshotMjpegChannelGroup);
662 if (streamingAutoFps) {
663 if (motionDetected) {
664 sendMjpegFrame(incommingSnapshot, autoSnapshotMjpegChannelGroup);
665 } else if (updateAutoFps) {
666 // only happens every 8 seconds as some browsers need a frame that often to keep stream alive.
667 sendMjpegFrame(incommingSnapshot, autoSnapshotMjpegChannelGroup);
668 updateAutoFps = false;
672 if (updateImageChannel) {
673 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
674 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
675 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
676 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
677 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
678 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
679 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
683 public void stopStreamServer() {
684 serversLoopGroup.shutdownGracefully();
685 serverBootstrap = null;
688 @SuppressWarnings("null")
689 public void startStreamServer() {
690 if (serverBootstrap == null) {
692 serversLoopGroup = new NioEventLoopGroup();
693 serverBootstrap = new ServerBootstrap();
694 serverBootstrap.group(serversLoopGroup);
695 serverBootstrap.channel(NioServerSocketChannel.class);
696 // IP "0.0.0.0" will bind the server to all network connections//
697 serverBootstrap.localAddress(new InetSocketAddress("0.0.0.0", cameraConfig.getServerPort()));
698 serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
700 protected void initChannel(SocketChannel socketChannel) throws Exception {
701 socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 60, 0));
702 socketChannel.pipeline().addLast("HttpServerCodec", new HttpServerCodec());
703 socketChannel.pipeline().addLast("ChunkedWriteHandler", new ChunkedWriteHandler());
704 socketChannel.pipeline().addLast("streamServerHandler", new StreamServerHandler(getHandle()));
707 serverFuture = serverBootstrap.bind().sync();
708 serverFuture.await(4000);
709 logger.debug("File server for camera at {} has started on port {} for all NIC's.", cameraConfig.getIp(),
710 cameraConfig.getServerPort());
711 updateState(CHANNEL_MJPEG_URL,
712 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.mjpeg"));
713 updateState(CHANNEL_HLS_URL,
714 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.m3u8"));
715 updateState(CHANNEL_IMAGE_URL,
716 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.jpg"));
717 } catch (Exception e) {
718 cameraConfigError("Exception when starting server. Try changing the Server Port to another number.");
720 if (thing.getThingTypeUID().getId().equals(INSTAR_THING)) {
721 logger.debug("Setting up the Alarm Server settings in the camera now");
723 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
724 + hostIp + "&-as_port=" + cameraConfig.getServerPort()
725 + "&-as_path=/instar&-as_queryattr1=&-as_queryval1=&-as_queryattr2=&-as_queryval2=&-as_queryattr3=&-as_queryval3=&-as_activequery=1&-as_auth=0&-as_query1=0&-as_query2=0&-as_query3=0");
730 public void setupSnapshotStreaming(boolean stream, ChannelHandlerContext ctx, boolean auto) {
732 sendMjpegFirstPacket(ctx);
734 autoSnapshotMjpegChannelGroup.add(ctx.channel());
735 lockCurrentSnapshot.lock();
737 sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
738 // iOS uses a FIFO? and needs two frames to display a pic
739 sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
741 lockCurrentSnapshot.unlock();
743 streamingAutoFps = true;
745 snapshotMjpegChannelGroup.add(ctx.channel());
746 lockCurrentSnapshot.lock();
748 sendMjpegFrame(currentSnapshot, snapshotMjpegChannelGroup);
750 lockCurrentSnapshot.unlock();
752 streamingSnapshotMjpeg = true;
753 startSnapshotPolling();
756 snapshotMjpegChannelGroup.remove(ctx.channel());
757 autoSnapshotMjpegChannelGroup.remove(ctx.channel());
758 if (streamingSnapshotMjpeg && snapshotMjpegChannelGroup.isEmpty()) {
759 streamingSnapshotMjpeg = false;
760 stopSnapshotPolling();
761 logger.debug("All snapshots.mjpeg streams have stopped.");
762 } else if (streamingAutoFps && autoSnapshotMjpegChannelGroup.isEmpty()) {
763 streamingAutoFps = false;
764 stopSnapshotPolling();
765 logger.debug("All autofps.mjpeg streams have stopped.");
770 // If start is true the CTX is added to the list to stream video to, false stops
772 public void setupMjpegStreaming(boolean start, ChannelHandlerContext ctx) {
774 if (mjpegChannelGroup.isEmpty()) {// first stream being requested.
775 mjpegChannelGroup.add(ctx.channel());
776 if (mjpegUri.isEmpty() || "ffmpeg".equals(mjpegUri)) {
777 sendMjpegFirstPacket(ctx);
778 setupFfmpegFormat(FFmpegFormat.MJPEG);
781 // fix Dahua reboots when refreshing a mjpeg stream.
782 TimeUnit.MILLISECONDS.sleep(500);
783 } catch (InterruptedException e) {
785 sendHttpGET(mjpegUri);
787 } else if (ffmpegMjpeg != null) {// not first stream and we will use ffmpeg
788 sendMjpegFirstPacket(ctx);
789 mjpegChannelGroup.add(ctx.channel());
790 } else {// not first stream and camera supplies the mjpeg source.
791 ctx.channel().writeAndFlush(firstStreamedMsg);
792 mjpegChannelGroup.add(ctx.channel());
795 mjpegChannelGroup.remove(ctx.channel());
796 if (mjpegChannelGroup.isEmpty()) {
797 logger.debug("All ipcamera.mjpeg streams have stopped.");
798 if ("ffmpeg".equals(mjpegUri) || mjpegUri.isEmpty()) {
799 Ffmpeg localMjpeg = ffmpegMjpeg;
800 if (localMjpeg != null) {
801 localMjpeg.stopConverting();
804 closeChannel(getTinyUrl(mjpegUri));
810 void openChannel(Channel channel, String httpRequestURL) {
811 ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
812 if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
813 tracker.setChannel(channel);
816 channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
819 void closeChannel(String url) {
820 ChannelTracking channelTracking = channelTrackingMap.get(url);
821 if (channelTracking != null) {
822 if (channelTracking.getChannel().isOpen()) {
823 channelTracking.getChannel().close();
830 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
831 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
834 void cleanChannels() {
835 for (Channel channel : openChannels) {
836 boolean oldChannel = true;
837 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
838 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
839 channelTrackingMap.remove(channelTracking.getRequestUrl());
841 if (channelTracking.getChannel() == channel) {
842 logger.trace("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
852 public void storeHttpReply(String url, String content) {
853 ChannelTracking channelTracking = channelTrackingMap.get(url);
854 if (channelTracking != null) {
855 channelTracking.setReply(content);
859 // sends direct to ctx so can be either snapshots.mjpeg or normal mjpeg stream
860 public void sendMjpegFirstPacket(ChannelHandlerContext ctx) {
861 final String boundary = "thisMjpegStream";
862 String contentType = "multipart/x-mixed-replace; boundary=" + boundary;
863 HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
864 response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
865 response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
866 response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
867 response.headers().add("Access-Control-Allow-Origin", "*");
868 response.headers().add("Access-Control-Expose-Headers", "*");
869 ctx.channel().writeAndFlush(response);
872 public void sendMjpegFrame(byte[] jpg, ChannelGroup channelGroup) {
873 final String boundary = "thisMjpegStream";
874 ByteBuf imageByteBuf = Unpooled.copiedBuffer(jpg);
875 int length = imageByteBuf.readableBytes();
876 String header = "--" + boundary + "\r\n" + "content-type: image/jpeg" + "\r\n" + "content-length: " + length
878 ByteBuf headerBbuf = Unpooled.copiedBuffer(header, 0, header.length(), StandardCharsets.UTF_8);
879 ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
880 streamToGroup(headerBbuf, channelGroup, false);
881 streamToGroup(imageByteBuf, channelGroup, false);
882 streamToGroup(footerBbuf, channelGroup, true);
885 public void streamToGroup(Object msg, ChannelGroup channelGroup, boolean flush) {
886 channelGroup.write(msg);
888 channelGroup.flush();
892 private void storeSnapshots() {
894 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
895 lockCurrentSnapshot.lock();
897 for (byte[] foo : fifoSnapshotBuffer) {
898 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
901 OutputStream fos = new FileOutputStream(file);
904 } catch (FileNotFoundException e) {
905 logger.warn("FileNotFoundException {}", e.getMessage());
906 } catch (IOException e) {
907 logger.warn("IOException {}", e.getMessage());
911 lockCurrentSnapshot.unlock();
915 public void setupFfmpegFormat(FFmpegFormat format) {
916 String inputOptions = cameraConfig.getFfmpegInputOptions();
917 if (cameraConfig.getFfmpegOutput().isEmpty()) {
918 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
921 if (rtspUri.isEmpty()) {
922 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
925 if (cameraConfig.getFfmpegLocation().isEmpty()) {
926 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
929 if (rtspUri.toLowerCase().contains("rtsp")) {
930 if (inputOptions.isEmpty()) {
931 inputOptions = "-rtsp_transport tcp";
935 // Make sure the folder exists, if not create it.
936 new File(cameraConfig.getFfmpegOutput()).mkdirs();
939 if (ffmpegHLS == null) {
940 if (!inputOptions.isEmpty()) {
941 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
942 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
943 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
944 cameraConfig.getUser(), cameraConfig.getPassword());
946 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
947 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
948 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
949 cameraConfig.getPassword());
952 Ffmpeg localHLS = ffmpegHLS;
953 if (localHLS != null) {
954 localHLS.startConverting();
958 if (cameraConfig.getGifPreroll() > 0) {
959 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
960 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
961 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
962 + cameraConfig.getGifOutOptions(),
963 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
964 cameraConfig.getPassword());
966 if (!inputOptions.isEmpty()) {
967 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
969 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
971 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
972 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
973 cameraConfig.getUser(), cameraConfig.getPassword());
975 if (cameraConfig.getGifPreroll() > 0) {
978 Ffmpeg localGIF = ffmpegGIF;
979 if (localGIF != null) {
980 localGIF.startConverting();
981 if (gifHistory.isEmpty()) {
982 gifHistory = gifFilename;
983 } else if (!"ipcamera".equals(gifFilename)) {
984 gifHistory = gifFilename + "," + gifHistory;
985 if (gifHistoryLength > 49) {
986 int endIndex = gifHistory.lastIndexOf(",");
987 gifHistory = gifHistory.substring(0, endIndex);
990 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
994 if (!inputOptions.isEmpty()) {
995 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
997 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
999 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1000 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
1001 cameraConfig.getUser(), cameraConfig.getPassword());
1002 Ffmpeg localRecord = ffmpegRecord;
1003 if (localRecord != null) {
1004 localRecord.startConverting();
1005 if (mp4History.isEmpty()) {
1006 mp4History = mp4Filename;
1007 } else if (!"ipcamera".equals(mp4Filename)) {
1008 mp4History = mp4Filename + "," + mp4History;
1009 if (mp4HistoryLength > 49) {
1010 int endIndex = mp4History.lastIndexOf(",");
1011 mp4History = mp4History.substring(0, endIndex);
1015 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1018 Ffmpeg localAlarms = ffmpegRtspHelper;
1019 if (localAlarms != null) {
1020 localAlarms.stopConverting();
1021 if (!audioAlarmEnabled && !motionAlarmEnabled) {
1025 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
1026 String filterOptions = "";
1027 if (!audioAlarmEnabled) {
1028 filterOptions = "-an";
1030 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
1032 if (!motionAlarmEnabled && !ffmpegSnapshotGeneration) {
1033 filterOptions = filterOptions.concat(" -vn");
1034 } else if (motionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
1035 String usersMotionOptions = cameraConfig.getMotionOptions();
1036 if (usersMotionOptions.startsWith("-")) {
1037 // Need to put the users custom options first in the chain before the motion is detected
1038 filterOptions += " " + usersMotionOptions + ",select='gte(scene," + motionThreshold
1039 + ")',metadata=print";
1041 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
1042 + motionThreshold + ")',metadata=print";
1044 } else if (motionAlarmEnabled) {
1045 filterOptions = filterOptions
1046 .concat(" -vf select='gte(scene," + motionThreshold + ")',metadata=print");
1048 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
1049 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
1050 localAlarms = ffmpegRtspHelper;
1051 if (localAlarms != null) {
1052 localAlarms.startConverting();
1056 if (ffmpegMjpeg == null) {
1057 if (inputOptions.isEmpty()) {
1058 inputOptions = "-hide_banner -loglevel warning";
1060 inputOptions += " -hide_banner -loglevel warning";
1062 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1063 cameraConfig.getMjpegOptions(),
1064 "http://127.0.0.1:" + cameraConfig.getServerPort() + "/ipcamera.jpg",
1065 cameraConfig.getUser(), cameraConfig.getPassword());
1067 Ffmpeg localMjpeg = ffmpegMjpeg;
1068 if (localMjpeg != null) {
1069 localMjpeg.startConverting();
1073 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
1074 if (ffmpegSnapshot == null) {
1075 if (inputOptions.isEmpty()) {
1077 inputOptions = "-threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1079 inputOptions += " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1081 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1082 cameraConfig.getSnapshotOptions(),
1083 "http://127.0.0.1:" + cameraConfig.getServerPort() + "/snapshot.jpg",
1084 cameraConfig.getUser(), cameraConfig.getPassword());
1086 Ffmpeg localSnaps = ffmpegSnapshot;
1087 if (localSnaps != null) {
1088 localSnaps.startConverting();
1094 public void noMotionDetected(String thisAlarmsChannel) {
1095 setChannelState(thisAlarmsChannel, OnOffType.OFF);
1096 firstMotionAlarm = false;
1097 motionAlarmUpdateSnapshot = false;
1098 motionDetected = false;
1099 if (streamingAutoFps) {
1100 stopSnapshotPolling();
1101 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1102 stopSnapshotPolling();
1107 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
1108 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
1109 * tampering with the camera.
1111 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
1112 updateState(thisAlarmsChannel, state);
1115 public void motionDetected(String thisAlarmsChannel) {
1116 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
1117 updateState(thisAlarmsChannel, OnOffType.ON);
1118 motionDetected = true;
1119 if (streamingAutoFps) {
1120 startSnapshotPolling();
1122 if (cameraConfig.getUpdateImageWhen().contains("2")) {
1123 if (!firstMotionAlarm) {
1124 if (!snapshotUri.isEmpty()) {
1125 sendHttpGET(snapshotUri);
1127 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1129 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1130 if (!snapshotPolling) {
1131 startSnapshotPolling();
1133 firstMotionAlarm = true;
1134 motionAlarmUpdateSnapshot = true;
1138 public void audioDetected() {
1139 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1140 if (cameraConfig.getUpdateImageWhen().contains("3")) {
1141 if (!firstAudioAlarm) {
1142 if (!snapshotUri.isEmpty()) {
1143 sendHttpGET(snapshotUri);
1145 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1147 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1148 firstAudioAlarm = true;
1149 audioAlarmUpdateSnapshot = true;
1153 public void noAudioDetected() {
1154 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1155 firstAudioAlarm = false;
1156 audioAlarmUpdateSnapshot = false;
1159 public void recordMp4(String filename, int seconds) {
1160 mp4Filename = filename;
1161 mp4RecordTime = seconds;
1162 setupFfmpegFormat(FFmpegFormat.RECORD);
1163 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1166 public void recordGif(String filename, int seconds) {
1167 gifFilename = filename;
1168 gifRecordTime = seconds;
1169 if (cameraConfig.getGifPreroll() > 0) {
1170 snapCount = seconds;
1172 setupFfmpegFormat(FFmpegFormat.GIF);
1174 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1177 public String returnValueFromString(String rawString, String searchedString) {
1179 int index = rawString.indexOf(searchedString);
1180 if (index != -1) // -1 means "not found"
1182 result = rawString.substring(index + searchedString.length(), rawString.length());
1183 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1185 return result; // Did not find a carriage return.
1187 return result.substring(0, index);
1190 return ""; // Did not find the String we were searching for
1193 private void sendPTZRequest() {
1194 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1198 public void channelLinked(ChannelUID channelUID) {
1199 if (cameraConfig.getServerPort() > 0) {
1200 switch (channelUID.getId()) {
1201 case CHANNEL_MJPEG_URL:
1202 updateState(CHANNEL_MJPEG_URL, new StringType(
1203 "http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.mjpeg"));
1205 case CHANNEL_HLS_URL:
1206 updateState(CHANNEL_HLS_URL,
1207 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.m3u8"));
1209 case CHANNEL_IMAGE_URL:
1210 updateState(CHANNEL_IMAGE_URL,
1211 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.jpg"));
1218 public void handleCommand(ChannelUID channelUID, Command command) {
1219 if (command instanceof RefreshType) {
1220 switch (channelUID.getId()) {
1222 if (onvifCamera.supportsPTZ()) {
1223 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1227 if (onvifCamera.supportsPTZ()) {
1228 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1232 if (onvifCamera.supportsPTZ()) {
1233 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1236 case CHANNEL_GOTO_PRESET:
1237 if (onvifCamera.supportsPTZ()) {
1238 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1242 } // caution "REFRESH" can still progress to brand Handlers below the else.
1244 switch (channelUID.getId()) {
1245 case CHANNEL_MP4_HISTORY_LENGTH:
1246 if (DecimalType.ZERO.equals(command)) {
1247 mp4HistoryLength = 0;
1249 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1252 case CHANNEL_GIF_HISTORY_LENGTH:
1253 if (DecimalType.ZERO.equals(command)) {
1254 gifHistoryLength = 0;
1256 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1259 case CHANNEL_FFMPEG_MOTION_CONTROL:
1260 if (OnOffType.ON.equals(command)) {
1261 motionAlarmEnabled = true;
1262 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1263 motionAlarmEnabled = false;
1264 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1266 motionAlarmEnabled = true;
1267 motionThreshold = Double.valueOf(command.toString());
1268 motionThreshold = motionThreshold / 10000;
1270 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1272 case CHANNEL_START_STREAM:
1274 if (OnOffType.ON.equals(command)) {
1275 localHLS = ffmpegHLS;
1276 if (localHLS == null) {
1277 setupFfmpegFormat(FFmpegFormat.HLS);
1278 localHLS = ffmpegHLS;
1280 if (localHLS != null) {
1281 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1282 localHLS.startConverting();
1285 localHLS = ffmpegHLS;
1286 if (localHLS != null) {
1287 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1288 localHLS.setKeepAlive(1);
1292 case CHANNEL_EXTERNAL_MOTION:
1293 if (OnOffType.ON.equals(command)) {
1294 motionDetected(CHANNEL_EXTERNAL_MOTION);
1296 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1299 case CHANNEL_GOTO_PRESET:
1300 if (onvifCamera.supportsPTZ()) {
1301 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1304 case CHANNEL_POLL_IMAGE:
1305 if (OnOffType.ON.equals(command)) {
1306 if (snapshotUri.isEmpty()) {
1307 ffmpegSnapshotGeneration = true;
1308 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1309 updateImageChannel = false;
1311 updateImageChannel = true;
1312 sendHttpGET(snapshotUri);// Allows this to change Image FPS on demand
1315 Ffmpeg localSnaps = ffmpegSnapshot;
1316 if (localSnaps != null) {
1317 localSnaps.stopConverting();
1318 ffmpegSnapshotGeneration = false;
1320 updateImageChannel = false;
1324 if (onvifCamera.supportsPTZ()) {
1325 if (command instanceof IncreaseDecreaseType) {
1326 if (command == IncreaseDecreaseType.INCREASE) {
1327 if (cameraConfig.getPtzContinuous()) {
1328 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1330 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1333 if (cameraConfig.getPtzContinuous()) {
1334 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1336 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1340 } else if (OnOffType.OFF.equals(command)) {
1341 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1344 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1345 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1349 if (onvifCamera.supportsPTZ()) {
1350 if (command instanceof IncreaseDecreaseType) {
1351 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1352 if (cameraConfig.getPtzContinuous()) {
1353 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1355 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1358 if (cameraConfig.getPtzContinuous()) {
1359 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1361 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1365 } else if (OnOffType.OFF.equals(command)) {
1366 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1369 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1370 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1374 if (onvifCamera.supportsPTZ()) {
1375 if (command instanceof IncreaseDecreaseType) {
1376 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1377 if (cameraConfig.getPtzContinuous()) {
1378 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1380 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1383 if (cameraConfig.getPtzContinuous()) {
1384 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1386 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1390 } else if (OnOffType.OFF.equals(command)) {
1391 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1394 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1395 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1400 // commands and refresh now get passed to brand handlers
1401 switch (thing.getThingTypeUID().getId()) {
1403 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1404 amcrestHandler.handleCommand(channelUID, command);
1405 if (lowPriorityRequests.isEmpty()) {
1406 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1410 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1411 dahuaHandler.handleCommand(channelUID, command);
1412 if (lowPriorityRequests.isEmpty()) {
1413 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1416 case DOORBIRD_THING:
1417 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1418 doorBirdHandler.handleCommand(channelUID, command);
1419 if (lowPriorityRequests.isEmpty()) {
1420 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1423 case HIKVISION_THING:
1424 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1425 hikvisionHandler.handleCommand(channelUID, command);
1426 if (lowPriorityRequests.isEmpty()) {
1427 lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1431 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1432 cameraConfig.getPassword());
1433 foscamHandler.handleCommand(channelUID, command);
1434 if (lowPriorityRequests.isEmpty()) {
1435 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1439 InstarHandler instarHandler = new InstarHandler(getHandle());
1440 instarHandler.handleCommand(channelUID, command);
1441 if (lowPriorityRequests.isEmpty()) {
1442 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1446 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1447 defaultHandler.handleCommand(channelUID, command);
1448 if (lowPriorityRequests.isEmpty()) {
1449 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1455 public void setChannelState(String channelToUpdate, State valueOf) {
1456 updateState(channelToUpdate, valueOf);
1459 private void bringCameraOnline() {
1461 updateStatus(ThingStatus.ONLINE);
1462 groupTracker.listOfOnlineCameraHandlers.add(this);
1463 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1464 Future<?> localFuture = cameraConnectionJob;
1465 if (localFuture != null) {
1466 localFuture.cancel(false);
1469 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1470 snapshotPolling = true;
1471 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000, cameraConfig.getPollTime(),
1472 TimeUnit.MILLISECONDS);
1475 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1477 if (!rtspUri.isEmpty()) {
1478 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1480 if (updateImageChannel) {
1481 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1483 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1485 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1486 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1487 handle.cameraOnline(getThing().getUID().getId());
1492 void snapshotIsFfmpeg() {
1493 bringCameraOnline();
1494 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1496 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1497 if (!rtspUri.isEmpty()) {
1498 updateImageChannel = false;
1499 ffmpegSnapshotGeneration = true;
1500 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1501 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1503 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1507 void pollingCameraConnection() {
1508 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1509 if (rtspUri.isEmpty()) {
1510 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1512 if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1515 sendHttpRequest("GET", snapshotUri, null);
1519 if (!onvifCamera.isConnected()) {
1520 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1521 cameraConfig.getOnvifPort());
1522 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1524 if ("ffmpeg".equals(snapshotUri)) {
1526 } else if (!snapshotUri.isEmpty()) {
1527 sendHttpRequest("GET", snapshotUri, null);
1528 } else if (!rtspUri.isEmpty()) {
1531 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1532 "Camera failed to report a valid Snaphot and/or RTSP URL. See readme on how to use the SNAPSHOT_URL_OVERRIDE feature.");
1536 public void cameraConfigError(String reason) {
1537 // wont try to reconnect again due to a config error being the cause.
1538 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1542 public void cameraCommunicationError(String reason) {
1543 // will try to reconnect again as camera may be rebooting.
1544 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1545 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1546 resetAndRetryConnecting();
1550 boolean streamIsStopped(String url) {
1551 ChannelTracking channelTracking = channelTrackingMap.get(url);
1552 if (channelTracking != null) {
1553 if (channelTracking.getChannel().isActive()) {
1554 return false; // stream is running.
1557 return true; // Stream stopped or never started.
1560 void snapshotRunnable() {
1561 // Snapshot should be first to keep consistent time between shots
1562 sendHttpGET(snapshotUri);
1563 if (snapCount > 0) {
1564 if (--snapCount == 0) {
1565 setupFfmpegFormat(FFmpegFormat.GIF);
1570 public void stopSnapshotPolling() {
1571 Future<?> localFuture;
1572 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1573 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1574 snapshotPolling = false;
1575 localFuture = snapshotJob;
1576 if (localFuture != null) {
1577 localFuture.cancel(true);
1579 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1580 snapshotPolling = false;
1581 localFuture = snapshotJob;
1582 if (localFuture != null) {
1583 localFuture.cancel(true);
1588 public void startSnapshotPolling() {
1589 if (snapshotPolling || ffmpegSnapshotGeneration) {
1590 return; // Already polling or creating with FFmpeg from RTSP
1592 if (streamingSnapshotMjpeg || streamingAutoFps) {
1593 snapshotPolling = true;
1594 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1595 TimeUnit.MILLISECONDS);
1596 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1597 snapshotPolling = true;
1598 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1599 TimeUnit.MILLISECONDS);
1604 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep mjpeg and alarm
1605 * streams open and more.
1608 void pollCameraRunnable() {
1609 // Snapshot should be first to keep consistent time between shots
1610 if (streamingAutoFps) {
1611 updateAutoFps = true;
1612 if (!snapshotPolling && !ffmpegSnapshotGeneration) {
1613 // Dont need to poll if creating from RTSP stream with FFmpeg or we are polling at full rate already.
1614 sendHttpGET(snapshotUri);
1616 } else if (!snapshotUri.isEmpty() && !snapshotPolling) {// we need to check camera is still online.
1617 sendHttpGET(snapshotUri);
1619 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1620 if (!lowPriorityRequests.isEmpty()) {
1621 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1622 lowPriorityCounter = 0;
1624 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1626 // what needs to be done every poll//
1627 switch (thing.getThingTypeUID().getId()) {
1631 if (!onvifCamera.isConnected()) {
1632 onvifCamera.connect(true);
1636 noMotionDetected(CHANNEL_MOTION_ALARM);
1637 noMotionDetected(CHANNEL_PIR_ALARM);
1640 case HIKVISION_THING:
1641 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1642 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1643 cameraConfig.getIp());
1644 sendHttpGET("/ISAPI/Event/notification/alertStream");
1648 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1649 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1652 // Check for alarms, channel for NVRs appears not to work at filtering.
1653 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1654 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1655 cameraConfig.getIp());
1656 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1659 case DOORBIRD_THING:
1660 // Check for alarms, channel for NVRs appears not to work at filtering.
1661 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1662 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1663 cameraConfig.getIp());
1664 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1668 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1669 + cameraConfig.getPassword());
1672 Ffmpeg localHLS = ffmpegHLS;
1673 if (localHLS != null) {
1674 localHLS.checkKeepAlive();
1676 if (openChannels.size() > 18) {
1677 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1683 public void initialize() {
1684 cameraConfig = getConfigAs(CameraConfig.class);
1685 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1686 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1687 rtspUri = cameraConfig.getFfmpegInput();
1688 if (cameraConfig.getFfmpegOutput().isEmpty()) {
1690 .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1693 if (cameraConfig.getServerPort() < 1) {
1695 "The Server Port is not set to a valid number which disables a lot of binding features. See readme for more info.");
1696 } else if (cameraConfig.getServerPort() < 1025) {
1697 logger.warn("The Server Port is <= 1024 and may cause permission errors under Linux, try a higher number.");
1700 // Known cameras will connect quicker if we skip ONVIF questions.
1701 switch (thing.getThingTypeUID().getId()) {
1704 if (mjpegUri.isEmpty()) {
1705 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1707 if (snapshotUri.isEmpty()) {
1708 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1711 case DOORBIRD_THING:
1712 if (mjpegUri.isEmpty()) {
1713 mjpegUri = "/bha-api/video.cgi";
1715 if (snapshotUri.isEmpty()) {
1716 snapshotUri = "/bha-api/image.cgi";
1720 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1721 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1722 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1723 if (mjpegUri.isEmpty()) {
1724 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1725 + cameraConfig.getPassword();
1727 if (snapshotUri.isEmpty()) {
1728 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1729 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1732 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1733 if (mjpegUri.isEmpty()) {
1734 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1736 if (snapshotUri.isEmpty()) {
1737 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1741 if (snapshotUri.isEmpty()) {
1742 snapshotUri = "/tmpfs/snap.jpg";
1744 if (mjpegUri.isEmpty()) {
1745 mjpegUri = "/mjpegstream.cgi?-chn=12";
1750 // Onvif and Instar event handling needs the host IP and the server started.
1751 if (cameraConfig.getServerPort() > 0) {
1752 startStreamServer();
1755 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1756 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1757 cameraConfig.getUser(), cameraConfig.getPassword());
1758 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1759 // Only use ONVIF events if it is not an API camera.
1760 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1763 // for poll times 9 seconds and above don't display a warning about the Image channel.
1764 if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1766 "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.");
1768 // Waiting 3 seconds for ONVIF to discover the urls before running.
1769 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 30, TimeUnit.SECONDS);
1772 // What the camera needs to re-connect if the initialize() is not called.
1773 private void resetAndRetryConnecting() {
1779 public void dispose() {
1781 snapshotPolling = false;
1782 onvifCamera.disconnect();
1783 Future<?> localFuture = pollCameraJob;
1784 if (localFuture != null) {
1785 localFuture.cancel(true);
1787 localFuture = snapshotJob;
1788 if (localFuture != null) {
1789 localFuture.cancel(true);
1791 localFuture = cameraConnectionJob;
1792 if (localFuture != null) {
1793 localFuture.cancel(true);
1795 threadPool.shutdown();
1796 threadPool = Executors.newScheduledThreadPool(4);
1798 groupTracker.listOfOnlineCameraHandlers.remove(this);
1799 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1800 // inform all group handlers that this camera has gone offline
1801 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1802 handle.cameraOffline(this);
1804 basicAuth = ""; // clear out stored Password hash
1805 useDigestAuth = false;
1807 openChannels.close();
1809 Ffmpeg localFfmpeg = ffmpegHLS;
1810 if (localFfmpeg != null) {
1811 localFfmpeg.stopConverting();
1814 localFfmpeg = ffmpegRecord;
1815 if (localFfmpeg != null) {
1816 localFfmpeg.stopConverting();
1818 localFfmpeg = ffmpegGIF;
1819 if (localFfmpeg != null) {
1820 localFfmpeg.stopConverting();
1822 localFfmpeg = ffmpegRtspHelper;
1823 if (localFfmpeg != null) {
1824 localFfmpeg.stopConverting();
1826 localFfmpeg = ffmpegMjpeg;
1827 if (localFfmpeg != null) {
1828 localFfmpeg.stopConverting();
1830 localFfmpeg = ffmpegSnapshot;
1831 if (localFfmpeg != null) {
1832 localFfmpeg.stopConverting();
1834 channelTrackingMap.clear();
1837 public void setStreamServerHandler(StreamServerHandler streamServerHandler2) {
1838 streamServerHandler = streamServerHandler2;
1841 public String getWhiteList() {
1842 return cameraConfig.getIpWhitelist();
1846 public Collection<Class<? extends ThingHandlerService>> getServices() {
1847 return Collections.singleton(IpCameraActions.class);