* 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>
}
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);
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);
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);
}
if (content.contains("motionsensor:H")) {
ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
}
-
} finally {
ReferenceCountUtil.release(msg);
}
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
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)) {
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>")) {
ctx.close();
ipCameraHandler.logger.debug("End of FOSCAM handler reached, so closing the channel to the camera now");
}
-
} finally {
ReferenceCountUtil.release(msg);
}
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>
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;
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")) {
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.");
}
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);
}
}
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);
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 = "";
}
// 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);
}
}
String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
- String outputOptions = "-f null -";
String filterOptions = "";
if (!audioAlarmEnabled) {
filterOptions = "-an";
}
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();
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.
}
}
}
}
- // 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()) {
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;
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
*/
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();) {
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) {
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
}).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();
}
}
<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>