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 // If start is true the CTX is added to the list to stream video to, false stops
773 public void setupMjpegStreaming(boolean start, ChannelHandlerContext ctx) {
775 if (mjpegChannelGroup.isEmpty()) {// first stream being requested.
776 mjpegChannelGroup.add(ctx.channel());
777 if (mjpegUri.isEmpty() || "ffmpeg".equals(mjpegUri)) {
778 sendMjpegFirstPacket(ctx);
779 setupFfmpegFormat(FFmpegFormat.MJPEG);
782 // fix Dahua reboots when refreshing a mjpeg stream.
783 TimeUnit.MILLISECONDS.sleep(500);
784 } catch (InterruptedException e) {
786 sendHttpGET(mjpegUri);
788 } else if (ffmpegMjpeg != null) {// not first stream and we will use ffmpeg
789 sendMjpegFirstPacket(ctx);
790 mjpegChannelGroup.add(ctx.channel());
791 } else {// not first stream and camera supplies the mjpeg source.
792 ctx.channel().writeAndFlush(firstStreamedMsg);
793 mjpegChannelGroup.add(ctx.channel());
796 mjpegChannelGroup.remove(ctx.channel());
797 if (mjpegChannelGroup.isEmpty()) {
798 logger.debug("All ipcamera.mjpeg streams have stopped.");
799 if ("ffmpeg".equals(mjpegUri) || mjpegUri.isEmpty()) {
800 Ffmpeg localMjpeg = ffmpegMjpeg;
801 if (localMjpeg != null) {
802 localMjpeg.stopConverting();
805 closeChannel(getTinyUrl(mjpegUri));
811 void openChannel(Channel channel, String httpRequestURL) {
812 ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
813 if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
814 tracker.setChannel(channel);
817 channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
820 void closeChannel(String url) {
821 ChannelTracking channelTracking = channelTrackingMap.get(url);
822 if (channelTracking != null) {
823 if (channelTracking.getChannel().isOpen()) {
824 channelTracking.getChannel().close();
831 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
832 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
835 void cleanChannels() {
836 for (Channel channel : openChannels) {
837 boolean oldChannel = true;
838 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
839 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
840 channelTrackingMap.remove(channelTracking.getRequestUrl());
842 if (channelTracking.getChannel() == channel) {
843 logger.trace("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
853 public void storeHttpReply(String url, String content) {
854 ChannelTracking channelTracking = channelTrackingMap.get(url);
855 if (channelTracking != null) {
856 channelTracking.setReply(content);
860 // sends direct to ctx so can be either snapshots.mjpeg or normal mjpeg stream
861 public void sendMjpegFirstPacket(ChannelHandlerContext ctx) {
862 final String boundary = "thisMjpegStream";
863 String contentType = "multipart/x-mixed-replace; boundary=" + boundary;
864 HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
865 response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
866 response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
867 response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
868 response.headers().add("Access-Control-Allow-Origin", "*");
869 response.headers().add("Access-Control-Expose-Headers", "*");
870 ctx.channel().writeAndFlush(response);
873 public void sendMjpegFrame(byte[] jpg, ChannelGroup channelGroup) {
874 final String boundary = "thisMjpegStream";
875 ByteBuf imageByteBuf = Unpooled.copiedBuffer(jpg);
876 int length = imageByteBuf.readableBytes();
877 String header = "--" + boundary + "\r\n" + "content-type: image/jpeg" + "\r\n" + "content-length: " + length
879 ByteBuf headerBbuf = Unpooled.copiedBuffer(header, 0, header.length(), StandardCharsets.UTF_8);
880 ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
881 streamToGroup(headerBbuf, channelGroup, false);
882 streamToGroup(imageByteBuf, channelGroup, false);
883 streamToGroup(footerBbuf, channelGroup, true);
886 public void streamToGroup(Object msg, ChannelGroup channelGroup, boolean flush) {
887 channelGroup.write(msg);
889 channelGroup.flush();
893 private void storeSnapshots() {
895 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
896 lockCurrentSnapshot.lock();
898 for (byte[] foo : fifoSnapshotBuffer) {
899 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
902 OutputStream fos = new FileOutputStream(file);
905 } catch (FileNotFoundException e) {
906 logger.warn("FileNotFoundException {}", e.getMessage());
907 } catch (IOException e) {
908 logger.warn("IOException {}", e.getMessage());
912 lockCurrentSnapshot.unlock();
916 public void setupFfmpegFormat(FFmpegFormat format) {
917 String inputOptions = cameraConfig.getFfmpegInputOptions();
918 if (cameraConfig.getFfmpegOutput().isEmpty()) {
919 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
922 if (rtspUri.isEmpty()) {
923 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
926 if (cameraConfig.getFfmpegLocation().isEmpty()) {
927 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
930 if (rtspUri.toLowerCase().contains("rtsp")) {
931 if (inputOptions.isEmpty()) {
932 inputOptions = "-rtsp_transport tcp";
936 // Make sure the folder exists, if not create it.
937 new File(cameraConfig.getFfmpegOutput()).mkdirs();
940 if (ffmpegHLS == null) {
941 if (!inputOptions.isEmpty()) {
942 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
943 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
944 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
945 cameraConfig.getUser(), cameraConfig.getPassword());
947 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
948 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
949 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
950 cameraConfig.getPassword());
953 Ffmpeg localHLS = ffmpegHLS;
954 if (localHLS != null) {
955 localHLS.startConverting();
959 if (cameraConfig.getGifPreroll() > 0) {
960 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
961 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
962 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
963 + cameraConfig.getGifOutOptions(),
964 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
965 cameraConfig.getPassword());
967 if (!inputOptions.isEmpty()) {
968 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
970 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
972 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
973 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
974 cameraConfig.getUser(), cameraConfig.getPassword());
976 if (cameraConfig.getGifPreroll() > 0) {
979 Ffmpeg localGIF = ffmpegGIF;
980 if (localGIF != null) {
981 localGIF.startConverting();
982 if (gifHistory.isEmpty()) {
983 gifHistory = gifFilename;
984 } else if (!"ipcamera".equals(gifFilename)) {
985 gifHistory = gifFilename + "," + gifHistory;
986 if (gifHistoryLength > 49) {
987 int endIndex = gifHistory.lastIndexOf(",");
988 gifHistory = gifHistory.substring(0, endIndex);
991 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
995 if (!inputOptions.isEmpty()) {
996 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
998 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
1000 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1001 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
1002 cameraConfig.getUser(), cameraConfig.getPassword());
1003 Ffmpeg localRecord = ffmpegRecord;
1004 if (localRecord != null) {
1005 localRecord.startConverting();
1006 if (mp4History.isEmpty()) {
1007 mp4History = mp4Filename;
1008 } else if (!"ipcamera".equals(mp4Filename)) {
1009 mp4History = mp4Filename + "," + mp4History;
1010 if (mp4HistoryLength > 49) {
1011 int endIndex = mp4History.lastIndexOf(",");
1012 mp4History = mp4History.substring(0, endIndex);
1016 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1019 Ffmpeg localAlarms = ffmpegRtspHelper;
1020 if (localAlarms != null) {
1021 localAlarms.stopConverting();
1022 if (!audioAlarmEnabled && !motionAlarmEnabled) {
1026 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
1027 String filterOptions = "";
1028 if (!audioAlarmEnabled) {
1029 filterOptions = "-an";
1031 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
1033 if (!motionAlarmEnabled && !ffmpegSnapshotGeneration) {
1034 filterOptions = filterOptions.concat(" -vn");
1035 } else if (motionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
1036 String usersMotionOptions = cameraConfig.getMotionOptions();
1037 if (usersMotionOptions.startsWith("-")) {
1038 // Need to put the users custom options first in the chain before the motion is detected
1039 filterOptions += " " + usersMotionOptions + ",select='gte(scene,"
1040 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
1042 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
1043 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
1045 } else if (motionAlarmEnabled) {
1046 filterOptions = filterOptions.concat(" -vf select='gte(scene,"
1047 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print");
1049 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
1050 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
1051 localAlarms = ffmpegRtspHelper;
1052 if (localAlarms != null) {
1053 localAlarms.startConverting();
1057 if (ffmpegMjpeg == null) {
1058 if (inputOptions.isEmpty()) {
1059 inputOptions = "-hide_banner -loglevel warning";
1061 inputOptions += " -hide_banner -loglevel warning";
1063 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1064 cameraConfig.getMjpegOptions(),
1065 "http://127.0.0.1:" + cameraConfig.getServerPort() + "/ipcamera.jpg",
1066 cameraConfig.getUser(), cameraConfig.getPassword());
1068 Ffmpeg localMjpeg = ffmpegMjpeg;
1069 if (localMjpeg != null) {
1070 localMjpeg.startConverting();
1074 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
1075 if (ffmpegSnapshot == null) {
1076 if (inputOptions.isEmpty()) {
1078 inputOptions = "-threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1080 inputOptions += " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1082 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1083 cameraConfig.getSnapshotOptions(),
1084 "http://127.0.0.1:" + cameraConfig.getServerPort() + "/snapshot.jpg",
1085 cameraConfig.getUser(), cameraConfig.getPassword());
1087 Ffmpeg localSnaps = ffmpegSnapshot;
1088 if (localSnaps != null) {
1089 localSnaps.startConverting();
1095 public void noMotionDetected(String thisAlarmsChannel) {
1096 setChannelState(thisAlarmsChannel, OnOffType.OFF);
1097 firstMotionAlarm = false;
1098 motionAlarmUpdateSnapshot = false;
1099 motionDetected = false;
1100 if (streamingAutoFps) {
1101 stopSnapshotPolling();
1102 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1103 stopSnapshotPolling();
1108 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
1109 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
1110 * tampering with the camera.
1112 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
1113 updateState(thisAlarmsChannel, state);
1116 public void motionDetected(String thisAlarmsChannel) {
1117 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
1118 updateState(thisAlarmsChannel, OnOffType.ON);
1119 motionDetected = true;
1120 if (streamingAutoFps) {
1121 startSnapshotPolling();
1123 if (cameraConfig.getUpdateImageWhen().contains("2")) {
1124 if (!firstMotionAlarm) {
1125 if (!snapshotUri.isEmpty()) {
1126 sendHttpGET(snapshotUri);
1128 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1130 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1131 if (!snapshotPolling) {
1132 startSnapshotPolling();
1134 firstMotionAlarm = true;
1135 motionAlarmUpdateSnapshot = true;
1139 public void audioDetected() {
1140 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1141 if (cameraConfig.getUpdateImageWhen().contains("3")) {
1142 if (!firstAudioAlarm) {
1143 if (!snapshotUri.isEmpty()) {
1144 sendHttpGET(snapshotUri);
1146 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1148 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1149 firstAudioAlarm = true;
1150 audioAlarmUpdateSnapshot = true;
1154 public void noAudioDetected() {
1155 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1156 firstAudioAlarm = false;
1157 audioAlarmUpdateSnapshot = false;
1160 public void recordMp4(String filename, int seconds) {
1161 mp4Filename = filename;
1162 mp4RecordTime = seconds;
1163 setupFfmpegFormat(FFmpegFormat.RECORD);
1164 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1167 public void recordGif(String filename, int seconds) {
1168 gifFilename = filename;
1169 gifRecordTime = seconds;
1170 if (cameraConfig.getGifPreroll() > 0) {
1171 snapCount = seconds;
1173 setupFfmpegFormat(FFmpegFormat.GIF);
1175 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1178 public String returnValueFromString(String rawString, String searchedString) {
1180 int index = rawString.indexOf(searchedString);
1181 if (index != -1) // -1 means "not found"
1183 result = rawString.substring(index + searchedString.length(), rawString.length());
1184 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1186 return result; // Did not find a carriage return.
1188 return result.substring(0, index);
1191 return ""; // Did not find the String we were searching for
1194 private void sendPTZRequest() {
1195 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1199 public void channelLinked(ChannelUID channelUID) {
1200 if (cameraConfig.getServerPort() > 0) {
1201 switch (channelUID.getId()) {
1202 case CHANNEL_MJPEG_URL:
1203 updateState(CHANNEL_MJPEG_URL, new StringType(
1204 "http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.mjpeg"));
1206 case CHANNEL_HLS_URL:
1207 updateState(CHANNEL_HLS_URL,
1208 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.m3u8"));
1210 case CHANNEL_IMAGE_URL:
1211 updateState(CHANNEL_IMAGE_URL,
1212 new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.jpg"));
1219 public void handleCommand(ChannelUID channelUID, Command command) {
1220 if (command instanceof RefreshType) {
1221 switch (channelUID.getId()) {
1223 if (onvifCamera.supportsPTZ()) {
1224 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1228 if (onvifCamera.supportsPTZ()) {
1229 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1233 if (onvifCamera.supportsPTZ()) {
1234 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1237 case CHANNEL_GOTO_PRESET:
1238 if (onvifCamera.supportsPTZ()) {
1239 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1243 } // caution "REFRESH" can still progress to brand Handlers below the else.
1245 switch (channelUID.getId()) {
1246 case CHANNEL_MP4_HISTORY_LENGTH:
1247 if (DecimalType.ZERO.equals(command)) {
1248 mp4HistoryLength = 0;
1250 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1253 case CHANNEL_GIF_HISTORY_LENGTH:
1254 if (DecimalType.ZERO.equals(command)) {
1255 gifHistoryLength = 0;
1257 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1260 case CHANNEL_FFMPEG_MOTION_CONTROL:
1261 if (OnOffType.ON.equals(command)) {
1262 motionAlarmEnabled = true;
1263 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1264 motionAlarmEnabled = false;
1265 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1266 } else if (command instanceof PercentType) {
1267 motionAlarmEnabled = true;
1268 motionThreshold = ((PercentType) command).toBigDecimal();
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);