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.math.BigDecimal;
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.Future;
36 import java.util.concurrent.ScheduledExecutorService;
37 import java.util.concurrent.ScheduledFuture;
38 import java.util.concurrent.TimeUnit;
39 import java.util.concurrent.locks.ReentrantLock;
41 import org.eclipse.jdt.annotation.NonNullByDefault;
42 import org.eclipse.jdt.annotation.Nullable;
43 import org.openhab.binding.ipcamera.internal.AmcrestHandler;
44 import org.openhab.binding.ipcamera.internal.CameraConfig;
45 import org.openhab.binding.ipcamera.internal.ChannelTracking;
46 import org.openhab.binding.ipcamera.internal.DahuaHandler;
47 import org.openhab.binding.ipcamera.internal.DoorBirdHandler;
48 import org.openhab.binding.ipcamera.internal.Ffmpeg;
49 import org.openhab.binding.ipcamera.internal.FoscamHandler;
50 import org.openhab.binding.ipcamera.internal.GroupTracker;
51 import org.openhab.binding.ipcamera.internal.Helper;
52 import org.openhab.binding.ipcamera.internal.HikvisionHandler;
53 import org.openhab.binding.ipcamera.internal.HttpOnlyHandler;
54 import org.openhab.binding.ipcamera.internal.InstarHandler;
55 import org.openhab.binding.ipcamera.internal.IpCameraActions;
56 import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
57 import org.openhab.binding.ipcamera.internal.IpCameraDynamicStateDescriptionProvider;
58 import org.openhab.binding.ipcamera.internal.MyNettyAuthHandler;
59 import org.openhab.binding.ipcamera.internal.StreamServerHandler;
60 import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection;
61 import org.openhab.core.OpenHAB;
62 import org.openhab.core.library.types.DecimalType;
63 import org.openhab.core.library.types.IncreaseDecreaseType;
64 import org.openhab.core.library.types.OnOffType;
65 import org.openhab.core.library.types.PercentType;
66 import org.openhab.core.library.types.RawType;
67 import org.openhab.core.library.types.StringType;
68 import org.openhab.core.thing.ChannelUID;
69 import org.openhab.core.thing.Thing;
70 import org.openhab.core.thing.ThingStatus;
71 import org.openhab.core.thing.ThingStatusDetail;
72 import org.openhab.core.thing.binding.BaseThingHandler;
73 import org.openhab.core.thing.binding.ThingHandlerService;
74 import org.openhab.core.types.Command;
75 import org.openhab.core.types.RefreshType;
76 import org.openhab.core.types.State;
77 import org.slf4j.Logger;
78 import org.slf4j.LoggerFactory;
80 import io.netty.bootstrap.Bootstrap;
81 import io.netty.bootstrap.ServerBootstrap;
82 import io.netty.buffer.ByteBuf;
83 import io.netty.buffer.Unpooled;
84 import io.netty.channel.Channel;
85 import io.netty.channel.ChannelDuplexHandler;
86 import io.netty.channel.ChannelFuture;
87 import io.netty.channel.ChannelFutureListener;
88 import io.netty.channel.ChannelHandlerContext;
89 import io.netty.channel.ChannelInitializer;
90 import io.netty.channel.ChannelOption;
91 import io.netty.channel.EventLoopGroup;
92 import io.netty.channel.group.ChannelGroup;
93 import io.netty.channel.group.DefaultChannelGroup;
94 import io.netty.channel.nio.NioEventLoopGroup;
95 import io.netty.channel.socket.SocketChannel;
96 import io.netty.channel.socket.nio.NioServerSocketChannel;
97 import io.netty.channel.socket.nio.NioSocketChannel;
98 import io.netty.handler.codec.base64.Base64;
99 import io.netty.handler.codec.http.DefaultFullHttpRequest;
100 import io.netty.handler.codec.http.DefaultHttpResponse;
101 import io.netty.handler.codec.http.FullHttpRequest;
102 import io.netty.handler.codec.http.HttpClientCodec;
103 import io.netty.handler.codec.http.HttpContent;
104 import io.netty.handler.codec.http.HttpHeaderNames;
105 import io.netty.handler.codec.http.HttpHeaderValues;
106 import io.netty.handler.codec.http.HttpMessage;
107 import io.netty.handler.codec.http.HttpMethod;
108 import io.netty.handler.codec.http.HttpResponse;
109 import io.netty.handler.codec.http.HttpResponseStatus;
110 import io.netty.handler.codec.http.HttpServerCodec;
111 import io.netty.handler.codec.http.HttpVersion;
112 import io.netty.handler.codec.http.LastHttpContent;
113 import io.netty.handler.stream.ChunkedWriteHandler;
114 import io.netty.handler.timeout.IdleState;
115 import io.netty.handler.timeout.IdleStateEvent;
116 import io.netty.handler.timeout.IdleStateHandler;
117 import io.netty.util.CharsetUtil;
118 import io.netty.util.ReferenceCountUtil;
119 import io.netty.util.concurrent.GlobalEventExecutor;
122 * The {@link IpCameraHandler} is responsible for handling commands, which are
123 * sent to one of the channels.
125 * @author Matthew Skinner - Initial contribution
129 public class IpCameraHandler extends BaseThingHandler {
130 public final Logger logger = LoggerFactory.getLogger(getClass());
131 public final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
132 private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(4);
133 private GroupTracker groupTracker;
134 public CameraConfig cameraConfig = new CameraConfig();
136 // ChannelGroup is thread safe
137 public final ChannelGroup mjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
138 private final ChannelGroup snapshotMjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
139 private final ChannelGroup autoSnapshotMjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
140 public final ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
141 public @Nullable Ffmpeg ffmpegHLS = null;
142 public @Nullable Ffmpeg ffmpegRecord = null;
143 public @Nullable Ffmpeg ffmpegGIF = null;
144 public @Nullable Ffmpeg ffmpegRtspHelper = null;
145 public @Nullable Ffmpeg ffmpegMjpeg = null;
146 public @Nullable Ffmpeg ffmpegSnapshot = null;
147 public boolean streamingAutoFps = false;
148 public boolean motionDetected = false;
150 private @Nullable ScheduledFuture<?> cameraConnectionJob = null;
151 private @Nullable ScheduledFuture<?> pollCameraJob = null;
152 private @Nullable ScheduledFuture<?> snapshotJob = null;
153 private @Nullable Bootstrap mainBootstrap;
154 private @Nullable ServerBootstrap serverBootstrap;
156 private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup();
157 private EventLoopGroup serversLoopGroup = new NioEventLoopGroup();
158 private FullHttpRequest putRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
160 private String gifFilename = "ipcamera";
161 private String gifHistory = "";
162 private String mp4History = "";
163 public int gifHistoryLength;
164 public int mp4HistoryLength;
165 private String mp4Filename = "ipcamera";
166 private int mp4RecordTime;
167 private int gifRecordTime = 5;
168 private LinkedList<byte[]> fifoSnapshotBuffer = new LinkedList<byte[]>();
169 private int snapCount;
170 private boolean updateImageChannel = false;
171 private boolean updateAutoFps = false;
172 private byte lowPriorityCounter = 0;
173 public String hostIp;
174 public Map<String, ChannelTracking> channelTrackingMap = new ConcurrentHashMap<>();
175 public List<String> lowPriorityRequests = new ArrayList<>(0);
177 // basicAuth MUST remain private as it holds the cameraConfig.getPassword()
178 private String basicAuth = "";
179 public boolean useBasicAuth = false;
180 public boolean useDigestAuth = false;
181 public String snapshotUri = "";
182 public String mjpegUri = "";
183 private @Nullable ChannelFuture serverFuture = null;
184 private Object firstStreamedMsg = new Object();
185 public byte[] currentSnapshot = new byte[] { (byte) 0x00 };
186 public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
187 public String rtspUri = "";
188 public boolean audioAlarmUpdateSnapshot = false;
189 private boolean motionAlarmUpdateSnapshot = false;
190 private boolean isOnline = false; // Used so only 1 error is logged when a network issue occurs.
191 private boolean firstAudioAlarm = false;
192 private boolean firstMotionAlarm = false;
193 public BigDecimal motionThreshold = BigDecimal.ZERO;
194 public int audioThreshold = 35;
195 @SuppressWarnings("unused")
196 private @Nullable StreamServerHandler streamServerHandler;
197 private boolean streamingSnapshotMjpeg = false;
198 public boolean motionAlarmEnabled = false;
199 public boolean audioAlarmEnabled = false;
200 public boolean ffmpegSnapshotGeneration = false;
201 public boolean snapshotPolling = false;
202 public OnvifConnection onvifCamera = new OnvifConnection(this, "", "", "");
204 // These methods handle the response from all camera brands, nothing specific to 1 brand.
205 private class CommonCameraHandler extends ChannelDuplexHandler {
206 private int bytesToRecieve = 0;
207 private int bytesAlreadyRecieved = 0;
208 private byte[] incomingJpeg = new byte[0];
209 private String incomingMessage = "";
210 private String contentType = "empty";
211 private String boundary = "";
212 private Object reply = new Object();
213 private String requestUrl = "";
214 private boolean closeConnection = true;
215 private boolean isChunked = false;
217 public void setURL(String url) {
222 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
223 if (msg == null || ctx == null) {
227 if (msg instanceof HttpResponse) {
228 HttpResponse response = (HttpResponse) msg;
229 if (response.status().code() != 401) {
230 if (!response.headers().isEmpty()) {
231 for (String name : response.headers().names()) {
232 // Some cameras use first letter uppercase and others dont.
233 switch (name.toLowerCase()) { // Possible localization issues doing this
235 contentType = response.headers().getAsString(name);
237 case "content-length":
238 bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
241 if (response.headers().getAsString(name).contains("keep-alive")) {
242 closeConnection = false;
245 case "transfer-encoding":
246 if (response.headers().getAsString(name).contains("chunked")) {
252 if (contentType.contains("multipart")) {
253 closeConnection = false;
254 if (mjpegUri.equals(requestUrl)) {
255 if (msg instanceof HttpMessage) {
256 // very start of stream only
257 ReferenceCountUtil.retain(msg, 1);
258 firstStreamedMsg = msg;
259 streamToGroup(firstStreamedMsg, mjpegChannelGroup, true);
262 boundary = Helper.searchString(contentType, "boundary=");
264 } else if (contentType.contains("image/jp")) {
265 if (bytesToRecieve == 0) {
266 bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
267 logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
269 incomingJpeg = new byte[bytesToRecieve];
274 if (msg instanceof HttpContent) {
275 if (mjpegUri.equals(requestUrl)) {
276 // multiple MJPEG stream packets come back as this.
277 ReferenceCountUtil.retain(msg, 1);
278 streamToGroup(msg, mjpegChannelGroup, true);
280 HttpContent content = (HttpContent) msg;
281 // Found some cameras use Content-Type: image/jpg instead of image/jpeg
282 if (contentType.contains("image/jp")) {
283 for (int i = 0; i < content.content().capacity(); i++) {
284 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
286 if (content instanceof LastHttpContent) {
287 processSnapshot(incomingJpeg);
288 // testing next line and if works need to do a full cleanup of this function.
289 closeConnection = true;
290 if (closeConnection) {
294 bytesAlreadyRecieved = 0;
297 } else { // incomingMessage that is not an IMAGE
298 if (incomingMessage.isEmpty()) {
299 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
301 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
303 bytesAlreadyRecieved = incomingMessage.length();
304 if (content instanceof LastHttpContent) {
305 // If it is not an image send it on to the next handler//
306 if (bytesAlreadyRecieved != 0) {
307 reply = incomingMessage;
308 super.channelRead(ctx, reply);
311 // Alarm Streams never have a LastHttpContent as they always stay open//
312 else if (contentType.contains("multipart")) {
313 int beginIndex, endIndex;
314 if (bytesToRecieve == 0) {
315 beginIndex = incomingMessage.indexOf("Content-Length:");
316 if (beginIndex != -1) {
317 endIndex = incomingMessage.indexOf("\r\n", beginIndex);
318 if (endIndex != -1) {
319 bytesToRecieve = Integer.parseInt(
320 incomingMessage.substring(beginIndex + 15, endIndex).strip());
324 // --boundary and headers are not included in the Content-Length value
325 if (bytesAlreadyRecieved > bytesToRecieve) {
326 // Check if message has a second --boundary
327 endIndex = incomingMessage.indexOf("--" + boundary, bytesToRecieve);
328 if (endIndex == -1) {
329 reply = incomingMessage;
330 incomingMessage = "";
332 bytesAlreadyRecieved = 0;
334 reply = incomingMessage.substring(0, endIndex);
335 incomingMessage = incomingMessage.substring(endIndex, incomingMessage.length());
336 bytesToRecieve = 0;// Triggers search next time for Content-Length:
337 bytesAlreadyRecieved = incomingMessage.length() - endIndex;
339 super.channelRead(ctx, reply);
342 // Foscam needs this as will other cameras with chunks//
343 if (isChunked && bytesAlreadyRecieved != 0) {
344 logger.debug("Reply is chunked.");
345 reply = incomingMessage;
346 super.channelRead(ctx, reply);
350 } else { // msg is not HttpContent
351 // Foscam cameras need this
352 if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
353 reply = incomingMessage;
354 logger.debug("Packet back from camera is {}", incomingMessage);
355 super.channelRead(ctx, reply);
359 ReferenceCountUtil.release(msg);
364 public void channelReadComplete(@Nullable ChannelHandlerContext ctx) {
368 public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
372 public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
376 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
377 if (cause == null || ctx == null) {
380 if (cause instanceof ArrayIndexOutOfBoundsException) {
381 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
384 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
391 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
395 if (evt instanceof IdleStateEvent) {
396 IdleStateEvent e = (IdleStateEvent) evt;
397 // If camera does not use the channel for X amount of time it will close.
398 if (e.state() == IdleState.READER_IDLE) {
399 String urlToKeepOpen = "";
400 switch (thing.getThingTypeUID().getId()) {
402 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
404 case HIKVISION_THING:
405 urlToKeepOpen = "/ISAPI/Event/notification/alertStream";
408 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
411 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
412 if (channelTracking != null) {
413 if (channelTracking.getChannel() == ctx.channel()) {
414 return; // don't auto close this as it is for the alarms.
423 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
424 IpCameraDynamicStateDescriptionProvider stateDescriptionProvider) {
426 this.stateDescriptionProvider = stateDescriptionProvider;
427 if (ipAddress != null) {
430 hostIp = Helper.getLocalIpAddress();
432 this.groupTracker = groupTracker;
435 private IpCameraHandler getHandle() {
439 // false clears the stored user/pass hash, true creates the hash
440 public boolean setBasicAuth(boolean useBasic) {
442 logger.debug("Clearing out the stored BASIC auth now.");
445 } else if (!basicAuth.isEmpty()) {
446 // due to camera may have been sent multiple requests before the auth was set, this may trigger falsely.
447 logger.warn("Camera is reporting your username and/or password is wrong.");
450 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
451 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
452 ByteBuf byteBuf = null;
454 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
455 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
457 if (byteBuf != null) {
463 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
468 private String getCorrectUrlFormat(String longUrl) {
469 String temp = longUrl;
472 if (longUrl.isEmpty() || "ffmpeg".equals(longUrl)) {
477 url = new URL(longUrl);
478 int port = url.getPort();
480 if (url.getQuery() == null) {
481 temp = url.getPath();
483 temp = url.getPath() + "?" + url.getQuery();
486 if (url.getQuery() == null) {
487 temp = ":" + url.getPort() + url.getPath();
489 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
492 } catch (MalformedURLException e) {
493 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
498 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
499 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
500 sendHttpRequest("PUT", httpRequestURL, null);
503 public void sendHttpGET(String httpRequestURL) {
504 sendHttpRequest("GET", httpRequestURL, null);
507 public int getPortFromShortenedUrl(String httpRequestURL) {
508 if (httpRequestURL.startsWith(":")) {
509 int end = httpRequestURL.indexOf("/");
510 return Integer.parseInt(httpRequestURL.substring(1, end));
512 return cameraConfig.getPort();
515 public String getTinyUrl(String httpRequestURL) {
516 if (httpRequestURL.startsWith(":")) {
517 int beginIndex = httpRequestURL.indexOf("/");
518 return httpRequestURL.substring(beginIndex);
520 return httpRequestURL;
523 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
524 // The authHandler will generate a digest string and re-send using this same function when needed.
525 @SuppressWarnings("null")
526 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
527 int port = getPortFromShortenedUrl(httpRequestURLFull);
528 String httpRequestURL = getTinyUrl(httpRequestURLFull);
530 if (mainBootstrap == null) {
531 mainBootstrap = new Bootstrap();
532 mainBootstrap.group(mainEventLoopGroup);
533 mainBootstrap.channel(NioSocketChannel.class);
534 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
535 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
536 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
537 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
538 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
539 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
542 public void initChannel(SocketChannel socketChannel) throws Exception {
543 // HIK Alarm stream needs > 9sec idle to stop stream closing
544 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
545 socketChannel.pipeline().addLast(new HttpClientCodec());
546 socketChannel.pipeline().addLast(AUTH_HANDLER,
547 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
548 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
550 switch (thing.getThingTypeUID().getId()) {
552 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
555 socketChannel.pipeline()
556 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
559 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
562 socketChannel.pipeline().addLast(
563 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
565 case HIKVISION_THING:
566 socketChannel.pipeline()
567 .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
570 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
573 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
580 FullHttpRequest request;
581 if (!"PUT".equals(httpMethod) || (useDigestAuth && digestString == null)) {
582 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
583 request.headers().set("Host", cameraConfig.getIp() + ":" + port);
584 request.headers().set("Connection", HttpHeaderValues.KEEP_ALIVE);
586 request = putRequestWithBody;
589 if (!basicAuth.isEmpty()) {
591 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
594 request.headers().set("Authorization", "Basic " + basicAuth);
599 if (digestString != null) {
600 request.headers().set("Authorization", "Digest " + digestString);
604 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
605 .addListener(new ChannelFutureListener() {
608 public void operationComplete(@Nullable ChannelFuture future) {
609 if (future == null) {
612 if (future.isDone() && future.isSuccess()) {
613 Channel ch = future.channel();
614 openChannels.add(ch);
618 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
621 openChannel(ch, httpRequestURL);
622 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
623 commonHandler.setURL(httpRequestURLFull);
624 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
625 authHandler.setURL(httpMethod, httpRequestURL);
627 switch (thing.getThingTypeUID().getId()) {
629 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
630 amcrestHandler.setURL(httpRequestURL);
633 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
634 instarHandler.setURL(httpRequestURL);
637 ch.writeAndFlush(request);
638 } else { // an error occured
639 cameraCommunicationError(
640 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
646 public void processSnapshot(byte[] incommingSnapshot) {
647 lockCurrentSnapshot.lock();
649 currentSnapshot = incommingSnapshot;
650 if (cameraConfig.getGifPreroll() > 0) {
651 fifoSnapshotBuffer.add(incommingSnapshot);
652 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
653 fifoSnapshotBuffer.removeFirst();
657 lockCurrentSnapshot.unlock();
660 if (streamingSnapshotMjpeg) {
661 sendMjpegFrame(incommingSnapshot, snapshotMjpegChannelGroup);
663 if (streamingAutoFps) {
664 if (motionDetected) {
665 sendMjpegFrame(incommingSnapshot, autoSnapshotMjpegChannelGroup);
666 } else if (updateAutoFps) {
667 // only happens every 8 seconds as some browsers need a frame that often to keep stream alive.
668 sendMjpegFrame(incommingSnapshot, autoSnapshotMjpegChannelGroup);
669 updateAutoFps = false;
673 if (updateImageChannel) {
674 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
675 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
676 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
677 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
678 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
679 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
680 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
684 public void stopStreamServer() {
685 serversLoopGroup.shutdownGracefully();
686 serverBootstrap = null;
689 @SuppressWarnings("null")
690 public void startStreamServer() {
691 if (serverBootstrap == null) {
693 serversLoopGroup = new NioEventLoopGroup();
694 serverBootstrap = new ServerBootstrap();
695 serverBootstrap.group(serversLoopGroup);
696 serverBootstrap.channel(NioServerSocketChannel.class);
697 // IP "0.0.0.0" will bind the server to all network connections//
698 serverBootstrap.localAddress(new InetSocketAddress("0.0.0.0", cameraConfig.getServerPort()));
699 serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
701 protected void initChannel(SocketChannel socketChannel) throws Exception {
702 socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 60, 0));
703 socketChannel.pipeline().addLast("HttpServerCodec", new HttpServerCodec());
704 socketChannel.pipeline().addLast("ChunkedWriteHandler", new ChunkedWriteHandler());
705 socketChannel.pipeline().addLast("streamServerHandler", new StreamServerHandler(getHandle()));
708 serverFuture = serverBootstrap.bind().sync();
709 serverFuture.await(4000);
710 logger.debug("File server for camera at {} has started on port {} for all NIC's.", cameraConfig.getIp(),
711 cameraConfig.getServerPort());
712 updateState(CHANNEL_MJPEG_URL,
713 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.mjpeg"));
714 updateState(CHANNEL_HLS_URL,
715 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.m3u8"));
716 updateState(CHANNEL_IMAGE_URL,
717 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.jpg"));
718 } catch (Exception e) {
719 cameraConfigError("Exception when starting server. Try changing the Server Port to another number.");
721 if (thing.getThingTypeUID().getId().equals(INSTAR_THING)) {
722 logger.debug("Setting up the Alarm Server settings in the camera now");
724 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
725 + hostIp + "&-as_port=" + cameraConfig.getServerPort()
726 + "&-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");
731 public void setupSnapshotStreaming(boolean stream, ChannelHandlerContext ctx, boolean auto) {
733 sendMjpegFirstPacket(ctx);
735 autoSnapshotMjpegChannelGroup.add(ctx.channel());
736 lockCurrentSnapshot.lock();
738 sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
739 // iOS uses a FIFO? and needs two frames to display a pic
740 sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
742 lockCurrentSnapshot.unlock();
744 streamingAutoFps = true;
746 snapshotMjpegChannelGroup.add(ctx.channel());
747 lockCurrentSnapshot.lock();
749 sendMjpegFrame(currentSnapshot, snapshotMjpegChannelGroup);
751 lockCurrentSnapshot.unlock();
753 streamingSnapshotMjpeg = true;
754 startSnapshotPolling();
757 snapshotMjpegChannelGroup.remove(ctx.channel());
758 autoSnapshotMjpegChannelGroup.remove(ctx.channel());
759 if (streamingSnapshotMjpeg && snapshotMjpegChannelGroup.isEmpty()) {
760 streamingSnapshotMjpeg = false;
761 stopSnapshotPolling();
762 logger.debug("All snapshots.mjpeg streams have stopped.");
763 } else if (streamingAutoFps && autoSnapshotMjpegChannelGroup.isEmpty()) {
764 streamingAutoFps = false;
765 stopSnapshotPolling();
766 logger.debug("All autofps.mjpeg streams have stopped.");
771 private void openMjpegStream() {
772 sendHttpGET(mjpegUri);
775 // If start is true the CTX is added to the list to stream video to, false stops
777 public void setupMjpegStreaming(boolean start, ChannelHandlerContext ctx) {
779 if (mjpegChannelGroup.isEmpty()) {// first stream being requested.
780 mjpegChannelGroup.add(ctx.channel());
781 if (mjpegUri.isEmpty() || "ffmpeg".equals(mjpegUri)) {
782 sendMjpegFirstPacket(ctx);
783 setupFfmpegFormat(FFmpegFormat.MJPEG);
784 } else {// Delay fixes Dahua reboots when refreshing a mjpeg stream.
785 threadPool.schedule(this::openMjpegStream, 500, TimeUnit.MILLISECONDS);
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,"
1039 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
1041 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
1042 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
1044 } else if (motionAlarmEnabled) {
1045 filterOptions = filterOptions.concat(" -vf select='gte(scene,"
1046 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',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);
1265 } else if (command instanceof PercentType) {
1266 motionAlarmEnabled = true;
1267 motionThreshold = ((PercentType) command).toBigDecimal();
1269 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1271 case CHANNEL_START_STREAM:
1273 if (OnOffType.ON.equals(command)) {
1274 localHLS = ffmpegHLS;
1275 if (localHLS == null) {
1276 setupFfmpegFormat(FFmpegFormat.HLS);
1277 localHLS = ffmpegHLS;
1279 if (localHLS != null) {
1280 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1281 localHLS.startConverting();
1284 localHLS = ffmpegHLS;
1285 if (localHLS != null) {
1286 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1287 localHLS.setKeepAlive(1);
1291 case CHANNEL_EXTERNAL_MOTION:
1292 if (OnOffType.ON.equals(command)) {
1293 motionDetected(CHANNEL_EXTERNAL_MOTION);
1295 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1298 case CHANNEL_GOTO_PRESET:
1299 if (onvifCamera.supportsPTZ()) {
1300 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1303 case CHANNEL_POLL_IMAGE:
1304 if (OnOffType.ON.equals(command)) {
1305 if (snapshotUri.isEmpty()) {
1306 ffmpegSnapshotGeneration = true;
1307 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1308 updateImageChannel = false;
1310 updateImageChannel = true;
1311 sendHttpGET(snapshotUri);// Allows this to change Image FPS on demand
1314 Ffmpeg localSnaps = ffmpegSnapshot;
1315 if (localSnaps != null) {
1316 localSnaps.stopConverting();
1317 ffmpegSnapshotGeneration = false;
1319 updateImageChannel = false;
1323 if (onvifCamera.supportsPTZ()) {
1324 if (command instanceof IncreaseDecreaseType) {
1325 if (command == IncreaseDecreaseType.INCREASE) {
1326 if (cameraConfig.getPtzContinuous()) {
1327 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1329 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1332 if (cameraConfig.getPtzContinuous()) {
1333 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1335 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1339 } else if (OnOffType.OFF.equals(command)) {
1340 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1343 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1344 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1348 if (onvifCamera.supportsPTZ()) {
1349 if (command instanceof IncreaseDecreaseType) {
1350 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1351 if (cameraConfig.getPtzContinuous()) {
1352 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1354 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1357 if (cameraConfig.getPtzContinuous()) {
1358 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1360 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1364 } else if (OnOffType.OFF.equals(command)) {
1365 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1368 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1369 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1373 if (onvifCamera.supportsPTZ()) {
1374 if (command instanceof IncreaseDecreaseType) {
1375 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1376 if (cameraConfig.getPtzContinuous()) {
1377 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1379 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1382 if (cameraConfig.getPtzContinuous()) {
1383 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1385 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1389 } else if (OnOffType.OFF.equals(command)) {
1390 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1393 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1394 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1399 // commands and refresh now get passed to brand handlers
1400 switch (thing.getThingTypeUID().getId()) {
1402 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1403 amcrestHandler.handleCommand(channelUID, command);
1404 if (lowPriorityRequests.isEmpty()) {
1405 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1409 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1410 dahuaHandler.handleCommand(channelUID, command);
1411 if (lowPriorityRequests.isEmpty()) {
1412 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1415 case DOORBIRD_THING:
1416 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1417 doorBirdHandler.handleCommand(channelUID, command);
1418 if (lowPriorityRequests.isEmpty()) {
1419 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1422 case HIKVISION_THING:
1423 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1424 hikvisionHandler.handleCommand(channelUID, command);
1425 if (lowPriorityRequests.isEmpty()) {
1426 lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1430 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1431 cameraConfig.getPassword());
1432 foscamHandler.handleCommand(channelUID, command);
1433 if (lowPriorityRequests.isEmpty()) {
1434 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1438 InstarHandler instarHandler = new InstarHandler(getHandle());
1439 instarHandler.handleCommand(channelUID, command);
1440 if (lowPriorityRequests.isEmpty()) {
1441 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1445 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1446 defaultHandler.handleCommand(channelUID, command);
1447 if (lowPriorityRequests.isEmpty()) {
1448 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1454 public void setChannelState(String channelToUpdate, State valueOf) {
1455 updateState(channelToUpdate, valueOf);
1458 private void bringCameraOnline() {
1460 updateStatus(ThingStatus.ONLINE);
1461 groupTracker.listOfOnlineCameraHandlers.add(this);
1462 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1463 Future<?> localFuture = cameraConnectionJob;
1464 if (localFuture != null) {
1465 localFuture.cancel(false);
1468 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1469 snapshotPolling = true;
1470 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000, cameraConfig.getPollTime(),
1471 TimeUnit.MILLISECONDS);
1474 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1476 if (!rtspUri.isEmpty()) {
1477 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1479 if (updateImageChannel) {
1480 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1482 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1484 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1485 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1486 handle.cameraOnline(getThing().getUID().getId());
1491 void snapshotIsFfmpeg() {
1492 bringCameraOnline();
1493 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1495 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1496 if (!rtspUri.isEmpty()) {
1497 updateImageChannel = false;
1498 ffmpegSnapshotGeneration = true;
1499 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1500 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1502 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1506 void pollingCameraConnection() {
1507 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1508 if (rtspUri.isEmpty()) {
1509 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1511 if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1514 sendHttpRequest("GET", snapshotUri, null);
1518 if (!onvifCamera.isConnected()) {
1519 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1520 cameraConfig.getOnvifPort());
1521 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1523 if ("ffmpeg".equals(snapshotUri)) {
1525 } else if (!snapshotUri.isEmpty()) {
1526 sendHttpRequest("GET", snapshotUri, null);
1527 } else if (!rtspUri.isEmpty()) {
1530 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1531 "Camera failed to report a valid Snaphot and/or RTSP URL. See readme on how to use the SNAPSHOT_URL_OVERRIDE feature.");
1535 public void cameraConfigError(String reason) {
1536 // wont try to reconnect again due to a config error being the cause.
1537 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1541 public void cameraCommunicationError(String reason) {
1542 // will try to reconnect again as camera may be rebooting.
1543 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1544 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1545 resetAndRetryConnecting();
1549 boolean streamIsStopped(String url) {
1550 ChannelTracking channelTracking = channelTrackingMap.get(url);
1551 if (channelTracking != null) {
1552 if (channelTracking.getChannel().isActive()) {
1553 return false; // stream is running.
1556 return true; // Stream stopped or never started.
1559 void snapshotRunnable() {
1560 // Snapshot should be first to keep consistent time between shots
1561 sendHttpGET(snapshotUri);
1562 if (snapCount > 0) {
1563 if (--snapCount == 0) {
1564 setupFfmpegFormat(FFmpegFormat.GIF);
1569 public void stopSnapshotPolling() {
1570 Future<?> localFuture;
1571 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1572 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1573 snapshotPolling = false;
1574 localFuture = snapshotJob;
1575 if (localFuture != null) {
1576 localFuture.cancel(true);
1578 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1579 snapshotPolling = false;
1580 localFuture = snapshotJob;
1581 if (localFuture != null) {
1582 localFuture.cancel(true);
1587 public void startSnapshotPolling() {
1588 if (snapshotPolling || ffmpegSnapshotGeneration) {
1589 return; // Already polling or creating with FFmpeg from RTSP
1591 if (streamingSnapshotMjpeg || streamingAutoFps) {
1592 snapshotPolling = true;
1593 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1594 TimeUnit.MILLISECONDS);
1595 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1596 snapshotPolling = true;
1597 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1598 TimeUnit.MILLISECONDS);
1603 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep mjpeg and alarm
1604 * streams open and more.
1607 void pollCameraRunnable() {
1608 // Snapshot should be first to keep consistent time between shots
1609 if (streamingAutoFps) {
1610 updateAutoFps = true;
1611 if (!snapshotPolling && !ffmpegSnapshotGeneration) {
1612 // Dont need to poll if creating from RTSP stream with FFmpeg or we are polling at full rate already.
1613 sendHttpGET(snapshotUri);
1615 } else if (!snapshotUri.isEmpty() && !snapshotPolling) {// we need to check camera is still online.
1616 sendHttpGET(snapshotUri);
1618 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1619 if (!lowPriorityRequests.isEmpty()) {
1620 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1621 lowPriorityCounter = 0;
1623 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1625 // what needs to be done every poll//
1626 switch (thing.getThingTypeUID().getId()) {
1630 if (!onvifCamera.isConnected()) {
1631 onvifCamera.connect(true);
1635 noMotionDetected(CHANNEL_MOTION_ALARM);
1636 noMotionDetected(CHANNEL_PIR_ALARM);
1639 case HIKVISION_THING:
1640 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1641 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1642 cameraConfig.getIp());
1643 sendHttpGET("/ISAPI/Event/notification/alertStream");
1647 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1648 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1651 // Check for alarms, channel for NVRs appears not to work at filtering.
1652 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1653 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1654 cameraConfig.getIp());
1655 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1658 case DOORBIRD_THING:
1659 // Check for alarms, channel for NVRs appears not to work at filtering.
1660 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1661 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1662 cameraConfig.getIp());
1663 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1667 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1668 + cameraConfig.getPassword());
1671 Ffmpeg localHLS = ffmpegHLS;
1672 if (localHLS != null) {
1673 localHLS.checkKeepAlive();
1675 if (openChannels.size() > 18) {
1676 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1682 public void initialize() {
1683 cameraConfig = getConfigAs(CameraConfig.class);
1684 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1685 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1686 rtspUri = cameraConfig.getFfmpegInput();
1687 if (cameraConfig.getFfmpegOutput().isEmpty()) {
1689 .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1692 if (cameraConfig.getServerPort() < 1) {
1694 "The Server Port is not set to a valid number which disables a lot of binding features. See readme for more info.");
1695 } else if (cameraConfig.getServerPort() < 1025) {
1696 logger.warn("The Server Port is <= 1024 and may cause permission errors under Linux, try a higher number.");
1699 // Known cameras will connect quicker if we skip ONVIF questions.
1700 switch (thing.getThingTypeUID().getId()) {
1703 if (mjpegUri.isEmpty()) {
1704 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1706 if (snapshotUri.isEmpty()) {
1707 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1710 case DOORBIRD_THING:
1711 if (mjpegUri.isEmpty()) {
1712 mjpegUri = "/bha-api/video.cgi";
1714 if (snapshotUri.isEmpty()) {
1715 snapshotUri = "/bha-api/image.cgi";
1719 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1720 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1721 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1722 if (mjpegUri.isEmpty()) {
1723 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1724 + cameraConfig.getPassword();
1726 if (snapshotUri.isEmpty()) {
1727 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1728 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1731 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1732 if (mjpegUri.isEmpty()) {
1733 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1735 if (snapshotUri.isEmpty()) {
1736 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1740 if (snapshotUri.isEmpty()) {
1741 snapshotUri = "/tmpfs/snap.jpg";
1743 if (mjpegUri.isEmpty()) {
1744 mjpegUri = "/mjpegstream.cgi?-chn=12";
1749 // Onvif and Instar event handling needs the host IP and the server started.
1750 if (cameraConfig.getServerPort() > 0) {
1751 startStreamServer();
1754 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1755 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1756 cameraConfig.getUser(), cameraConfig.getPassword());
1757 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1758 // Only use ONVIF events if it is not an API camera.
1759 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1762 // for poll times 9 seconds and above don't display a warning about the Image channel.
1763 if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1765 "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.");
1767 // Waiting 3 seconds for ONVIF to discover the urls before running.
1768 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 30, TimeUnit.SECONDS);
1771 // What the camera needs to re-connect if the initialize() is not called.
1772 private void resetAndRetryConnecting() {
1778 public void dispose() {
1780 snapshotPolling = false;
1781 Future<?> localFuture = pollCameraJob;
1782 if (localFuture != null) {
1783 localFuture.cancel(true);
1785 localFuture = snapshotJob;
1786 if (localFuture != null) {
1787 localFuture.cancel(true);
1789 localFuture = cameraConnectionJob;
1790 if (localFuture != null) {
1791 localFuture.cancel(true);
1793 threadPool.shutdown();
1794 threadPool = Executors.newScheduledThreadPool(4);
1796 groupTracker.listOfOnlineCameraHandlers.remove(this);
1797 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1798 // inform all group handlers that this camera has gone offline
1799 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1800 handle.cameraOffline(this);
1802 basicAuth = ""; // clear out stored Password hash
1803 useDigestAuth = false;
1805 openChannels.close();
1807 Ffmpeg localFfmpeg = ffmpegHLS;
1808 if (localFfmpeg != null) {
1809 localFfmpeg.stopConverting();
1812 localFfmpeg = ffmpegRecord;
1813 if (localFfmpeg != null) {
1814 localFfmpeg.stopConverting();
1816 localFfmpeg = ffmpegGIF;
1817 if (localFfmpeg != null) {
1818 localFfmpeg.stopConverting();
1820 localFfmpeg = ffmpegRtspHelper;
1821 if (localFfmpeg != null) {
1822 localFfmpeg.stopConverting();
1824 localFfmpeg = ffmpegMjpeg;
1825 if (localFfmpeg != null) {
1826 localFfmpeg.stopConverting();
1828 localFfmpeg = ffmpegSnapshot;
1829 if (localFfmpeg != null) {
1830 localFfmpeg.stopConverting();
1832 channelTrackingMap.clear();
1833 onvifCamera.disconnect();
1836 public void setStreamServerHandler(StreamServerHandler streamServerHandler2) {
1837 streamServerHandler = streamServerHandler2;
1840 public String getWhiteList() {
1841 return cameraConfig.getIpWhitelist();
1845 public Collection<Class<? extends ThingHandlerService>> getServices() {
1846 return Collections.singleton(IpCameraActions.class);