[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
Matthew Skinner 2020-12-07 16:56:25 +11:00 committed by GitHub
parent b8dc504f84
commit 84995bac83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 82 additions and 99 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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);
}

View File

@ -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)) {

View File

@ -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);
}

View File

@ -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;

View File

@ -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")) {

View File

@ -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.");

View File

@ -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()) {

View File

@ -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();
}
}

View File

@ -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>