[ipcamera] Improve ONVIF discovery and bug fixes. (#9199)
* Fix Offline detection and Improve discovery. * Motion options bug fix. * Message content bug fix. * Fix all handlers to process all chunks as one. * Remove password from FFmpeg command log. Signed-off-by: Matthew Skinner <matt@pcmus.com>pull/9274/head
parent
b8dc504f84
commit
84995bac83
|
@ -61,10 +61,7 @@ public class AmcrestHandler extends ChannelDuplexHandler {
|
|||
}
|
||||
try {
|
||||
String content = msg.toString();
|
||||
|
||||
if (!content.isEmpty()) {
|
||||
ipCameraHandler.logger.trace("HTTP Result back from camera is \t:{}:", content);
|
||||
}
|
||||
ipCameraHandler.logger.trace("HTTP Result back from camera is \t:{}:", content);
|
||||
if (content.contains("Error: No Events")) {
|
||||
if ("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion".equals(requestUrl)) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
|
|
|
@ -55,11 +55,9 @@ public class DahuaHandler extends ChannelDuplexHandler {
|
|||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
String content = msg.toString();
|
||||
try {
|
||||
if (!content.isEmpty()) {
|
||||
ipCameraHandler.logger.trace("HTTP Result back from camera is \t:{}:", content);
|
||||
}
|
||||
String content = msg.toString();
|
||||
ipCameraHandler.logger.trace("HTTP Result back from camera is \t:{}:", content);
|
||||
// determine if the motion detection is turned on or off.
|
||||
if (content.contains("table.MotionDetect[0].Enable=true")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.ON);
|
||||
|
|
|
@ -51,13 +51,9 @@ public class DoorBirdHandler extends ChannelDuplexHandler {
|
|||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
String content = msg.toString();
|
||||
try {
|
||||
if (!content.isEmpty()) {
|
||||
ipCameraHandler.logger.trace("HTTP Result back from camera is \t:{}:", content);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
String content = msg.toString();
|
||||
ipCameraHandler.logger.trace("HTTP Result back from camera is \t:{}:", content);
|
||||
if (content.contains("doorbell:H")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_DOORBELL, OnOffType.ON);
|
||||
}
|
||||
|
@ -70,7 +66,6 @@ public class DoorBirdHandler extends ChannelDuplexHandler {
|
|||
if (content.contains("motionsensor:H")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
|
||||
}
|
||||
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
|
|
|
@ -53,10 +53,12 @@ public class Ffmpeg {
|
|||
private IpCameraFfmpegThread ipCameraFfmpegThread = new IpCameraFfmpegThread();
|
||||
private int keepAlive = 8;
|
||||
private boolean running = false;
|
||||
private String password;
|
||||
|
||||
public Ffmpeg(IpCameraHandler handle, FFmpegFormat format, String ffmpegLocation, String inputArguments,
|
||||
String input, String outArguments, String output, String username, String password) {
|
||||
this.format = format;
|
||||
this.password = password;
|
||||
ipCameraHandler = handle;
|
||||
String altInput = input;
|
||||
// Input can be snapshots not just rtsp or http
|
||||
|
@ -169,7 +171,7 @@ public class Ffmpeg {
|
|||
public void startConverting() {
|
||||
if (!ipCameraFfmpegThread.isAlive()) {
|
||||
ipCameraFfmpegThread = new IpCameraFfmpegThread();
|
||||
logger.debug("Starting ffmpeg with this command now:{}", ffmpegCommand);
|
||||
logger.debug("Starting ffmpeg with this command now:{}", ffmpegCommand.replaceAll(password, "********"));
|
||||
ipCameraFfmpegThread.start();
|
||||
running = true;
|
||||
if (format.equals(FFmpegFormat.HLS)) {
|
||||
|
|
|
@ -57,14 +57,9 @@ public class FoscamHandler extends ChannelDuplexHandler {
|
|||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
String content = msg.toString();
|
||||
try {
|
||||
if (!content.isEmpty()) {
|
||||
ipCameraHandler.logger.trace("HTTP Result back from camera is \t:{}:", content);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
String content = msg.toString();
|
||||
ipCameraHandler.logger.trace("HTTP Result back from camera is \t:{}:", content);
|
||||
////////////// Motion Alarm //////////////
|
||||
if (content.contains("<motionDetectAlarm>")) {
|
||||
if (content.contains("<motionDetectAlarm>0</motionDetectAlarm>")) {
|
||||
|
@ -115,7 +110,6 @@ public class FoscamHandler extends ChannelDuplexHandler {
|
|||
ctx.close();
|
||||
ipCameraHandler.logger.debug("End of FOSCAM handler reached, so closing the channel to the camera now");
|
||||
}
|
||||
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
|
|
|
@ -67,15 +67,10 @@ public class HikvisionHandler extends ChannelDuplexHandler {
|
|||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
String content = "";
|
||||
int debounce = 3;
|
||||
try {
|
||||
content = msg.toString();
|
||||
if (content.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int debounce = 3;
|
||||
String content = msg.toString();
|
||||
logger.trace("HTTP Result back from camera is \t:{}:", content);
|
||||
|
||||
if (content.contains("--boundary")) {// Alarm checking goes in here//
|
||||
if (content.contains("<EventNotificationAlert version=\"")) {
|
||||
if (content.contains("hannelID>" + nvrChannel + "</")) {// some camera use c or <dynChannelID>
|
||||
|
@ -114,7 +109,8 @@ public class HikvisionHandler extends ChannelDuplexHandler {
|
|||
countDown();
|
||||
countDown();
|
||||
}
|
||||
} else if (content.contains("<channelID>0</channelID>")) {// NVR uses channel 0 to say all channels
|
||||
} else if (content.contains("<channelID>0</channelID>")) {// NVR uses channel 0 to say all
|
||||
// channels
|
||||
if (content.contains("<eventType>videoloss</eventType>\r\n<eventState>inactive</eventState>")) {
|
||||
if (vmdCount > 1) {
|
||||
vmdCount = 1;
|
||||
|
|
|
@ -58,13 +58,10 @@ public class InstarHandler extends ChannelDuplexHandler {
|
|||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
String content = "";
|
||||
String value1 = "";
|
||||
try {
|
||||
content = msg.toString();
|
||||
if (content.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String value1 = "";
|
||||
String content = msg.toString();
|
||||
ipCameraHandler.logger.trace("HTTP Result back from camera is \t:{}:", content);
|
||||
switch (requestUrl) {
|
||||
case "/param.cgi?cmd=getinfrared":
|
||||
if (content.contains("var infraredstat=\"auto")) {
|
||||
|
|
|
@ -68,7 +68,7 @@ public class IpCameraDiscoveryService extends AbstractDiscoveryService {
|
|||
removeOlderResults(getTimestampOfLastScan());
|
||||
OnvifDiscovery onvifDiscovery = new OnvifDiscovery(this);
|
||||
try {
|
||||
onvifDiscovery.discoverCameras(3702);// WS discovery
|
||||
onvifDiscovery.discoverCameras();
|
||||
} catch (UnknownHostException | InterruptedException e) {
|
||||
logger.warn(
|
||||
"IpCamera Discovery has an issue discovering the network settings to find cameras with. Try setting up the camera manually.");
|
||||
|
|
|
@ -249,7 +249,7 @@ public class IpCameraHandler extends BaseThingHandler {
|
|||
}
|
||||
if (contentType.contains("multipart")) {
|
||||
closeConnection = false;
|
||||
if (mjpegUri.contains(requestUrl)) {
|
||||
if (mjpegUri.equals(requestUrl)) {
|
||||
if (msg instanceof HttpMessage) {
|
||||
// very start of stream only
|
||||
ReferenceCountUtil.retain(msg, 1);
|
||||
|
@ -268,13 +268,13 @@ public class IpCameraHandler extends BaseThingHandler {
|
|||
}
|
||||
}
|
||||
if (msg instanceof HttpContent) {
|
||||
if (mjpegUri.contains(requestUrl)) {
|
||||
if (mjpegUri.equals(requestUrl)) {
|
||||
// multiple MJPEG stream packets come back as this.
|
||||
ReferenceCountUtil.retain(msg, 1);
|
||||
streamToGroup(msg, mjpegChannelGroup, true);
|
||||
} else {
|
||||
HttpContent content = (HttpContent) msg;
|
||||
// Found some cameras uses Content-Type: image/jpg instead of image/jpeg
|
||||
// Found some cameras use Content-Type: image/jpg instead of image/jpeg
|
||||
if (contentType.contains("image/jp")) {
|
||||
for (int i = 0; i < content.content().capacity(); i++) {
|
||||
incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
|
||||
|
@ -304,8 +304,8 @@ public class IpCameraHandler extends BaseThingHandler {
|
|||
super.channelRead(ctx, reply);
|
||||
}
|
||||
}
|
||||
// HIKVISION alertStream never has a LastHttpContent as it always stays open//
|
||||
if (contentType.contains("multipart")) {
|
||||
// Alarm Streams never have a LastHttpContent as they always stay open//
|
||||
else if (contentType.contains("multipart")) {
|
||||
if (bytesAlreadyRecieved != 0) {
|
||||
reply = incomingMessage;
|
||||
incomingMessage = "";
|
||||
|
@ -316,13 +316,14 @@ public class IpCameraHandler extends BaseThingHandler {
|
|||
}
|
||||
// Foscam needs this as will other cameras with chunks//
|
||||
if (isChunked && bytesAlreadyRecieved != 0) {
|
||||
logger.debug("Reply is chunked.");
|
||||
reply = incomingMessage;
|
||||
super.channelRead(ctx, reply);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // msg is not HttpContent
|
||||
// Foscam and Amcrest cameras need this
|
||||
// Foscam cameras need this
|
||||
if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
|
||||
reply = incomingMessage;
|
||||
logger.debug("Packet back from camera is {}", incomingMessage);
|
||||
|
@ -982,7 +983,6 @@ public class IpCameraHandler extends BaseThingHandler {
|
|||
}
|
||||
}
|
||||
String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
|
||||
String outputOptions = "-f null -";
|
||||
String filterOptions = "";
|
||||
if (!audioAlarmEnabled) {
|
||||
filterOptions = "-an";
|
||||
|
@ -991,16 +991,22 @@ public class IpCameraHandler extends BaseThingHandler {
|
|||
}
|
||||
if (!motionAlarmEnabled && !ffmpegSnapshotGeneration) {
|
||||
filterOptions = filterOptions.concat(" -vn");
|
||||
} else if (motionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
|
||||
String usersMotionOptions = cameraConfig.getMotionOptions();
|
||||
if (usersMotionOptions.startsWith("-")) {
|
||||
// Need to put the users custom options first in the chain before the motion is detected
|
||||
filterOptions += " " + usersMotionOptions + ",select='gte(scene," + motionThreshold
|
||||
+ ")',metadata=print";
|
||||
} else {
|
||||
filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
|
||||
+ motionThreshold + ")',metadata=print";
|
||||
}
|
||||
} else if (motionAlarmEnabled) {
|
||||
filterOptions = filterOptions
|
||||
.concat(" -vf select='gte(scene," + motionThreshold + ")',metadata=print");
|
||||
}
|
||||
if (!cameraConfig.getUser().isEmpty()) {
|
||||
filterOptions += " ";// add space as the Framework does not allow spaces at start of config.
|
||||
}
|
||||
ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
|
||||
filterOptions + cameraConfig.getMotionOptions(), outputOptions, cameraConfig.getUser(),
|
||||
cameraConfig.getPassword());
|
||||
filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
|
||||
localAlarms = ffmpegRtspHelper;
|
||||
if (localAlarms != null) {
|
||||
localAlarms.startConverting();
|
||||
|
@ -1484,7 +1490,7 @@ public class IpCameraHandler extends BaseThingHandler {
|
|||
boolean streamIsStopped(String url) {
|
||||
ChannelTracking channelTracking = channelTrackingMap.get(url);
|
||||
if (channelTracking != null) {
|
||||
if (channelTracking.getChannel().isOpen()) {
|
||||
if (channelTracking.getChannel().isActive()) {
|
||||
return false; // stream is running.
|
||||
}
|
||||
}
|
||||
|
@ -1534,20 +1540,21 @@ public class IpCameraHandler extends BaseThingHandler {
|
|||
}
|
||||
}
|
||||
|
||||
// runs every 8 seconds due to mjpeg streams not staying open unless they update this often.
|
||||
/**
|
||||
* {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep mjpeg and alarm
|
||||
* streams open and more.
|
||||
*
|
||||
*/
|
||||
void pollCameraRunnable() {
|
||||
// Snapshot should be first to keep consistent time between shots
|
||||
if (!snapshotUri.isEmpty()) {
|
||||
if (updateImageChannel) {
|
||||
sendHttpGET(snapshotUri);
|
||||
}
|
||||
}
|
||||
if (streamingAutoFps) {
|
||||
updateAutoFps = true;
|
||||
if (!snapshotPolling && !ffmpegSnapshotGeneration) {
|
||||
// Dont need to poll if creating from RTSP stream with FFmpeg or we are polling at full rate already.
|
||||
sendHttpGET(snapshotUri);
|
||||
}
|
||||
} else if (!snapshotUri.isEmpty() && !snapshotPolling) {// we need to check camera is still online.
|
||||
sendHttpGET(snapshotUri);
|
||||
}
|
||||
// NOTE: Use lowPriorityRequests if get request is not needed every poll.
|
||||
if (!lowPriorityRequests.isEmpty()) {
|
||||
|
|
|
@ -28,6 +28,7 @@ import java.net.UnknownHostException;
|
|||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
@ -42,19 +43,21 @@ import io.netty.bootstrap.Bootstrap;
|
|||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelFactory;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.channel.group.ChannelGroup;
|
||||
import io.netty.channel.group.DefaultChannelGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.DatagramChannel;
|
||||
import io.netty.channel.socket.DatagramPacket;
|
||||
import io.netty.channel.socket.InternetProtocolFamily;
|
||||
import io.netty.channel.socket.nio.NioDatagramChannel;
|
||||
import io.netty.util.CharsetUtil;
|
||||
import io.netty.util.concurrent.GlobalEventExecutor;
|
||||
|
||||
/**
|
||||
* The {@link OnvifDiscovery} is responsible for finding cameras that are Onvif using UDP multicast.
|
||||
* The {@link OnvifDiscovery} is responsible for finding cameras that are ONVIF using UDP multicast.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
@ -69,7 +72,8 @@ public class OnvifDiscovery {
|
|||
this.ipCameraDiscoveryService = ipCameraDiscoveryService;
|
||||
}
|
||||
|
||||
public @Nullable NetworkInterface getLocalNIF() {
|
||||
public @Nullable List<NetworkInterface> getLocalNICs() {
|
||||
List<NetworkInterface> results = new ArrayList<>(2);
|
||||
try {
|
||||
for (Enumeration<NetworkInterface> enumNetworks = NetworkInterface.getNetworkInterfaces(); enumNetworks
|
||||
.hasMoreElements();) {
|
||||
|
@ -79,13 +83,13 @@ public class OnvifDiscovery {
|
|||
InetAddress inetAddress = enumIpAddr.nextElement();
|
||||
if (!inetAddress.isLoopbackAddress() && inetAddress.getHostAddress().toString().length() < 18
|
||||
&& inetAddress.isSiteLocalAddress()) {
|
||||
return networkInterface;
|
||||
results.add(networkInterface);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SocketException ex) {
|
||||
}
|
||||
return null;
|
||||
return results;
|
||||
}
|
||||
|
||||
void searchReply(String url, String xml) {
|
||||
|
@ -180,23 +184,21 @@ public class OnvifDiscovery {
|
|||
return brand;
|
||||
}
|
||||
|
||||
public void discoverCameras(int port) throws UnknownHostException, InterruptedException {
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
String xml = "";
|
||||
|
||||
if (port == 3702) {
|
||||
xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><e:Envelope xmlns:e=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:w=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" xmlns:d=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\" xmlns:dn=\"http://www.onvif.org/ver10/network/wsdl\"><e:Header><w:MessageID>uuid:"
|
||||
+ uuid
|
||||
+ "</w:MessageID><w:To e:mustUnderstand=\"true\">urn:schemas-xmlsoap-org:ws:2005:04:discovery</w:To><w:Action a:mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</w:Action></e:Header><e:Body><d:Probe><d:Types xmlns:dp0=\"http://www.onvif.org/ver10/network/wsdl\">dp0:NetworkVideoTransmitter</d:Types></d:Probe></e:Body></e:Envelope>";
|
||||
}
|
||||
private DatagramPacket wsDiscovery() throws UnknownHostException {
|
||||
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><e:Envelope xmlns:e=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:w=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" xmlns:d=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\" xmlns:dn=\"http://www.onvif.org/ver10/network/wsdl\"><e:Header><w:MessageID>uuid:"
|
||||
+ UUID.randomUUID()
|
||||
+ "</w:MessageID><w:To e:mustUnderstand=\"true\">urn:schemas-xmlsoap-org:ws:2005:04:discovery</w:To><w:Action a:mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</w:Action></e:Header><e:Body><d:Probe><d:Types xmlns:d=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\" xmlns:dp0=\"http://www.onvif.org/ver10/network/wsdl\">dp0:NetworkVideoTransmitter</d:Types></d:Probe></e:Body></e:Envelope>";
|
||||
ByteBuf discoveryProbeMessage = Unpooled.copiedBuffer(xml, 0, xml.length(), StandardCharsets.UTF_8);
|
||||
InetSocketAddress localNetworkAddress = new InetSocketAddress(0);// Listen for replies on all connections.
|
||||
InetSocketAddress multiCastAddress = new InetSocketAddress(InetAddress.getByName("239.255.255.250"), port);
|
||||
DatagramPacket datagramPacket = new DatagramPacket(discoveryProbeMessage, multiCastAddress,
|
||||
localNetworkAddress);
|
||||
NetworkInterface networkInterface = getLocalNIF();
|
||||
DatagramChannel datagramChannel;
|
||||
return new DatagramPacket(discoveryProbeMessage,
|
||||
new InetSocketAddress(InetAddress.getByName("239.255.255.250"), 3702), new InetSocketAddress(0));
|
||||
}
|
||||
|
||||
public void discoverCameras() throws UnknownHostException, InterruptedException {
|
||||
List<NetworkInterface> nics = getLocalNICs();
|
||||
if (nics == null || nics.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
NetworkInterface networkInterface = nics.get(0);
|
||||
Bootstrap bootstrap = new Bootstrap().group(new NioEventLoopGroup())
|
||||
.channelFactory(new ChannelFactory<NioDatagramChannel>() {
|
||||
@Override
|
||||
|
@ -213,26 +215,21 @@ public class OnvifDiscovery {
|
|||
}).option(ChannelOption.SO_BROADCAST, true).option(ChannelOption.SO_REUSEADDR, true)
|
||||
.option(ChannelOption.IP_MULTICAST_LOOP_DISABLED, false).option(ChannelOption.SO_RCVBUF, 2048)
|
||||
.option(ChannelOption.IP_MULTICAST_TTL, 255).option(ChannelOption.IP_MULTICAST_IF, networkInterface);
|
||||
|
||||
datagramChannel = (DatagramChannel) bootstrap.bind(localNetworkAddress).sync().channel();
|
||||
datagramChannel.joinGroup(multiCastAddress, networkInterface).sync();
|
||||
ChannelFuture chFuture;
|
||||
if (port == 1900) {
|
||||
String ssdp = "M-SEARCH * HTTP/1.1\n" + "HOST: 239.255.255.250:1900\n" + "MAN: \"ssdp:discover\"\n"
|
||||
+ "MX: 1\n" + "ST: urn:dial-multiscreen-org:service:dial:1\n"
|
||||
+ "USER-AGENT: Microsoft Edge/83.0.478.61 Windows\n" + "\n" + "";
|
||||
ByteBuf ssdpProbeMessage = Unpooled.copiedBuffer(ssdp, 0, ssdp.length(), StandardCharsets.UTF_8);
|
||||
datagramPacket = new DatagramPacket(ssdpProbeMessage, multiCastAddress, localNetworkAddress);
|
||||
chFuture = datagramChannel.writeAndFlush(datagramPacket);
|
||||
} else {
|
||||
chFuture = datagramChannel.writeAndFlush(datagramPacket);
|
||||
ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
|
||||
for (NetworkInterface nic : nics) {
|
||||
DatagramChannel datagramChannel = (DatagramChannel) bootstrap.option(ChannelOption.IP_MULTICAST_IF, nic)
|
||||
.bind(new InetSocketAddress(0)).sync().channel();
|
||||
datagramChannel
|
||||
.joinGroup(new InetSocketAddress(InetAddress.getByName("239.255.255.250"), 3702), networkInterface)
|
||||
.sync();
|
||||
openChannels.add(datagramChannel);
|
||||
}
|
||||
if (!openChannels.isEmpty()) {
|
||||
openChannels.writeAndFlush(wsDiscovery());
|
||||
TimeUnit.SECONDS.sleep(6);
|
||||
openChannels.close();
|
||||
processCameraReplys();
|
||||
bootstrap.config().group().shutdownGracefully();
|
||||
}
|
||||
chFuture.awaitUninterruptibly(2000);
|
||||
chFuture = datagramChannel.closeFuture();
|
||||
TimeUnit.SECONDS.sleep(5);
|
||||
datagramChannel.close();
|
||||
chFuture.awaitUninterruptibly(6000);
|
||||
processCameraReplys();
|
||||
bootstrap.config().group().shutdownGracefully();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2665,7 +2665,7 @@
|
|||
<category>Light</category>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="enablePrivacyMode">
|
||||
<channel-type id="enablePrivacyMode" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Enable Privacy Mode</label>
|
||||
<description>Turn the Privacy Mode on and off.</description>
|
||||
|
|
Loading…
Reference in New Issue