[ipcamera] Improvements and fix 503 errors go to offline with Hik (#11419)

* Stop hik logging 401 with digest.

Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Improve and fix generic cams


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Stop dahua IntelliFrame logging


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Catch IllegalStateException


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Trial reusing channels.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Tidy up


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* cleanup 2


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Cleanup 3


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Disable checking connection with event stream.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Bug fix


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* more cleanup


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* more cleanup

Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Reduce logging to only whats needed.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* fix offline detection.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* fixes to ipcamera.mjpeg


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* reverse some connection checks


Signed-off-by: Matthew Skinner <matt@pcmus.com>
This commit is contained in:
Matthew Skinner 2021-10-24 20:36:20 +11:00 committed by GitHub
parent fb2263622b
commit 04ba8d3e5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 125 additions and 96 deletions

View File

@ -15,6 +15,7 @@ package org.openhab.binding.ipcamera.internal;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import io.netty.channel.Channel; import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
/** /**
* The {@link ChannelTracking} Can be used to find the handle for a HTTP channel if you know the URL. The reply can * The {@link ChannelTracking} Can be used to find the handle for a HTTP channel if you know the URL. The reply can
@ -43,6 +44,15 @@ public class ChannelTracking {
return channel; return channel;
} }
/**
* Closes the channel, but keeps the HTTP reply stored in the tracker.
*
* @return ChannelFuture
*/
public ChannelFuture closeChannel() {
return channel.close();
}
public String getReply() { public String getReply() {
return storedReply; return storedReply;
} }

View File

@ -49,7 +49,7 @@ public class DahuaHandler extends ChannelDuplexHandler {
} }
private void processEvent(String content) { private void processEvent(String content) {
int startIndex = content.indexOf("Code=", 12) + 5;// skip --myboundary int startIndex = content.indexOf("Code=", 12) + 5;// skip --myboundary and Code=
int endIndex = content.indexOf(";", startIndex + 1); int endIndex = content.indexOf(";", startIndex + 1);
if (startIndex == -1 || endIndex == -1) { if (startIndex == -1 || endIndex == -1) {
ipCameraHandler.logger.debug("Code= not found in Dahua event. Content was:{}", content); ipCameraHandler.logger.debug("Code= not found in Dahua event. Content was:{}", content);
@ -177,7 +177,9 @@ public class DahuaHandler extends ChannelDuplexHandler {
case "LensMaskClose": case "LensMaskClose":
ipCameraHandler.setChannelState(CHANNEL_ENABLE_PRIVACY_MODE, OnOffType.OFF); ipCameraHandler.setChannelState(CHANNEL_ENABLE_PRIVACY_MODE, OnOffType.OFF);
break; break;
// Skip these so they are not logged.
case "TimeChange": case "TimeChange":
case "IntelliFrame":
case "NTPAdjustTime": case "NTPAdjustTime":
case "StorageChange": case "StorageChange":
case "Reboot": case "Reboot":

View File

@ -96,7 +96,7 @@ public class Ffmpeg {
} }
private class IpCameraFfmpegThread extends Thread { private class IpCameraFfmpegThread extends Thread {
private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2); private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(1);
public int countOfMotions; public int countOfMotions;
IpCameraFfmpegThread() { IpCameraFfmpegThread() {
@ -220,6 +220,7 @@ public class Ffmpeg {
Process localProcess = process; Process localProcess = process;
if (localProcess != null) { if (localProcess != null) {
localProcess.destroyForcibly(); localProcess.destroyForcibly();
process = null;
} }
if (format.equals(FFmpegFormat.HLS)) { if (format.equals(FFmpegFormat.HLS)) {
ipCameraHandler.setChannelState(CHANNEL_START_STREAM, OnOffType.OFF); ipCameraHandler.setChannelState(CHANNEL_START_STREAM, OnOffType.OFF);

View File

@ -196,9 +196,6 @@ public class HikvisionHandler extends ChannelDuplexHandler {
} }
} }
break; break;
default:
logger.debug("Unhandled reply-{}.", content);
break;
} }
} }
} finally { } finally {

View File

@ -89,7 +89,9 @@ public class MyNettyAuthHandler extends ChannelDuplexHandler {
/////// Fresh Digest Authenticate method follows as Basic is already handled and returned //////// /////// Fresh Digest Authenticate method follows as Basic is already handled and returned ////////
realm = Helper.searchString(authenticate, "realm=\""); realm = Helper.searchString(authenticate, "realm=\"");
if (realm.isEmpty()) { if (realm.isEmpty()) {
logger.warn("Could not find a valid WWW-Authenticate response in :{}", authenticate); logger.warn(
"No valid WWW-Authenticate in response. Has the camera activated the illegal login lock? Details:{}",
authenticate);
return; return;
} }
nonce = Helper.searchString(authenticate, "nonce=\""); nonce = Helper.searchString(authenticate, "nonce=\"");
@ -142,23 +144,17 @@ public class MyNettyAuthHandler extends ChannelDuplexHandler {
if (msg == null || ctx == null) { if (msg == null || ctx == null) {
return; return;
} }
boolean closeConnection = true;
String authenticate = "";
if (msg instanceof HttpResponse) { if (msg instanceof HttpResponse) {
HttpResponse response = (HttpResponse) msg; HttpResponse response = (HttpResponse) msg;
if (response.status().code() == 401) { if (response.status().code() == 401) {
ctx.close();
if (!response.headers().isEmpty()) { if (!response.headers().isEmpty()) {
String authenticate = "";
for (CharSequence name : response.headers().names()) { for (CharSequence name : response.headers().names()) {
for (CharSequence value : response.headers().getAll(name)) { for (CharSequence value : response.headers().getAll(name)) {
if (name.toString().equalsIgnoreCase("WWW-Authenticate")) { if (name.toString().equalsIgnoreCase("WWW-Authenticate")) {
authenticate = value.toString(); authenticate = value.toString();
} }
if (name.toString().equalsIgnoreCase("Connection")
&& value.toString().contains("keep-alive")) {
// closeConnection = false;
// trial this for a while to see if it solves too many bytes with digest turned on.
closeConnection = true;
}
} }
} }
if (!authenticate.isEmpty()) { if (!authenticate.isEmpty()) {
@ -167,24 +163,22 @@ public class MyNettyAuthHandler extends ChannelDuplexHandler {
ipCameraHandler.cameraConfigError( ipCameraHandler.cameraConfigError(
"Camera gave no WWW-Authenticate: Your login details must be wrong."); "Camera gave no WWW-Authenticate: Your login details must be wrong.");
} }
if (closeConnection) {
ctx.close();// needs to be here
}
} }
} else if (response.status().code() != 200) { } else if (response.status().code() != 200) {
logger.debug("Camera at IP:{} gave a reply with a response code of :{}", ctx.close();
ipCameraHandler.cameraConfig.getIp(), response.status().code()); switch (response.status().code()) {
case 403:
logger.warn(
"403 Forbidden: Check camera setup or has the camera activated the illegal login lock?");
break;
default:
logger.debug("Camera at IP:{} gave a reply with a response code of :{}",
ipCameraHandler.cameraConfig.getIp(), response.status().code());
break;
}
} }
} }
// Pass the Message back to the pipeline for the next handler to process// // Pass the Message back to the pipeline for the next handler to process//
super.channelRead(ctx, msg); super.channelRead(ctx, msg);
} }
@Override
public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
}
@Override
public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
}
} }

View File

@ -23,6 +23,7 @@ import java.math.BigDecimal;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@ -123,7 +124,7 @@ import io.netty.util.concurrent.GlobalEventExecutor;
public class IpCameraHandler extends BaseThingHandler { public class IpCameraHandler extends BaseThingHandler {
public final Logger logger = LoggerFactory.getLogger(getClass()); public final Logger logger = LoggerFactory.getLogger(getClass());
public final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider; public final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(4); private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2);
private GroupTracker groupTracker; private GroupTracker groupTracker;
public CameraConfig cameraConfig = new CameraConfig(); public CameraConfig cameraConfig = new CameraConfig();
@ -140,6 +141,7 @@ public class IpCameraHandler extends BaseThingHandler {
public @Nullable Ffmpeg ffmpegSnapshot = null; public @Nullable Ffmpeg ffmpegSnapshot = null;
public boolean streamingAutoFps = false; public boolean streamingAutoFps = false;
public boolean motionDetected = false; public boolean motionDetected = false;
public Instant lastSnapshotRequest = Instant.now();
public Instant currentSnapshotTime = Instant.now(); public Instant currentSnapshotTime = Instant.now();
private @Nullable ScheduledFuture<?> cameraConnectionJob = null; private @Nullable ScheduledFuture<?> cameraConnectionJob = null;
private @Nullable ScheduledFuture<?> pollCameraJob = null; private @Nullable ScheduledFuture<?> pollCameraJob = null;
@ -197,7 +199,6 @@ public class IpCameraHandler extends BaseThingHandler {
private String boundary = ""; private String boundary = "";
private Object reply = new Object(); private Object reply = new Object();
private String requestUrl = ""; private String requestUrl = "";
private boolean closeConnection = true;
private boolean isChunked = false; private boolean isChunked = false;
public void setURL(String url) { public void setURL(String url) {
@ -223,11 +224,6 @@ public class IpCameraHandler extends BaseThingHandler {
case "content-length": case "content-length":
bytesToRecieve = Integer.parseInt(response.headers().getAsString(name)); bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
break; break;
case "connection":
if (response.headers().getAsString(name).contains("keep-alive")) {
closeConnection = false;
}
break;
case "transfer-encoding": case "transfer-encoding":
if (response.headers().getAsString(name).contains("chunked")) { if (response.headers().getAsString(name).contains("chunked")) {
isChunked = true; isChunked = true;
@ -236,7 +232,6 @@ public class IpCameraHandler extends BaseThingHandler {
} }
} }
if (contentType.contains("multipart")) { if (contentType.contains("multipart")) {
closeConnection = false;
if (mjpegUri.equals(requestUrl)) { if (mjpegUri.equals(requestUrl)) {
if (msg instanceof HttpMessage) { if (msg instanceof HttpMessage) {
// very start of stream only // very start of stream only
@ -278,14 +273,7 @@ public class IpCameraHandler extends BaseThingHandler {
} }
if (content instanceof LastHttpContent) { if (content instanceof LastHttpContent) {
processSnapshot(incomingJpeg); processSnapshot(incomingJpeg);
// testing next line and if works need to do a full cleanup of this function. ctx.close();
closeConnection = true;
if (closeConnection) {
ctx.close();
} else {
bytesToRecieve = 0;
bytesAlreadyRecieved = 0;
}
} }
} else { // incomingMessage that is not an IMAGE } else { // incomingMessage that is not an IMAGE
if (incomingMessage.isEmpty()) { if (incomingMessage.isEmpty()) {
@ -353,18 +341,6 @@ public class IpCameraHandler extends BaseThingHandler {
} }
} }
@Override
public void channelReadComplete(@Nullable ChannelHandlerContext ctx) {
}
@Override
public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
}
@Override
public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
}
@Override @Override
public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) { public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
if (cause == null || ctx == null) { if (cause == null || ctx == null) {
@ -394,9 +370,6 @@ public class IpCameraHandler extends BaseThingHandler {
case DAHUA_THING: case DAHUA_THING:
urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]"; urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
break; break;
case HIKVISION_THING:
urlToKeepOpen = "/ISAPI/Event/notification/alertStream";
break;
case DOORBIRD_THING: case DOORBIRD_THING:
urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor"; urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
break; break;
@ -407,6 +380,7 @@ public class IpCameraHandler extends BaseThingHandler {
return; // don't auto close this as it is for the alarms. return; // don't auto close this as it is for the alarms.
} }
} }
logger.debug("Closing an idle channel for camera:{}", cameraConfig.getIp());
ctx.close(); ctx.close();
} }
} }
@ -515,6 +489,10 @@ public class IpCameraHandler extends BaseThingHandler {
} }
private void checkCameraConnection() { private void checkCameraConnection() {
if (snapshotUri.isEmpty() || snapshotPolling) {
// Already polling or camera has RTSP only and no HTTP server
return;
}
Bootstrap localBootstrap = mainBootstrap; Bootstrap localBootstrap = mainBootstrap;
if (localBootstrap != null) { if (localBootstrap != null) {
ChannelFuture chFuture = localBootstrap ChannelFuture chFuture = localBootstrap
@ -688,7 +666,8 @@ public class IpCameraHandler extends BaseThingHandler {
} }
public void openCamerasStream() { public void openCamerasStream() {
threadPool.schedule(this::openMjpegStream, 500, TimeUnit.MILLISECONDS); closeChannel(getTinyUrl(mjpegUri));
mainEventLoopGroup.schedule(this::openMjpegStream, 0, TimeUnit.MILLISECONDS);
} }
private void openMjpegStream() { private void openMjpegStream() {
@ -719,7 +698,7 @@ public class IpCameraHandler extends BaseThingHandler {
* open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
* still occurring. * still occurring.
*/ */
void cleanChannels() { private void cleanChannels() {
for (Channel channel : openChannels) { for (Channel channel : openChannels) {
boolean oldChannel = true; boolean oldChannel = true;
for (ChannelTracking channelTracking : channelTrackingMap.values()) { for (ChannelTracking channelTracking : channelTrackingMap.values()) {
@ -727,7 +706,7 @@ public class IpCameraHandler extends BaseThingHandler {
channelTrackingMap.remove(channelTracking.getRequestUrl()); channelTrackingMap.remove(channelTracking.getRequestUrl());
} }
if (channelTracking.getChannel() == channel) { if (channelTracking.getChannel() == channel) {
logger.trace("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl()); logger.debug("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
oldChannel = false; oldChannel = false;
} }
} }
@ -977,7 +956,7 @@ public class IpCameraHandler extends BaseThingHandler {
if (cameraConfig.getUpdateImageWhen().contains("2")) { if (cameraConfig.getUpdateImageWhen().contains("2")) {
if (!firstMotionAlarm) { if (!firstMotionAlarm) {
if (!snapshotUri.isEmpty()) { if (!snapshotUri.isEmpty()) {
sendHttpGET(snapshotUri); updateSnapshot();
} }
firstMotionAlarm = true;// reset back to false when the jpg arrives. firstMotionAlarm = true;// reset back to false when the jpg arrives.
} }
@ -995,7 +974,7 @@ public class IpCameraHandler extends BaseThingHandler {
if (cameraConfig.getUpdateImageWhen().contains("3")) { if (cameraConfig.getUpdateImageWhen().contains("3")) {
if (!firstAudioAlarm) { if (!firstAudioAlarm) {
if (!snapshotUri.isEmpty()) { if (!snapshotUri.isEmpty()) {
sendHttpGET(snapshotUri); updateSnapshot();
} }
firstAudioAlarm = true;// reset back to false when the jpg arrives. firstAudioAlarm = true;// reset back to false when the jpg arrives.
} }
@ -1161,7 +1140,7 @@ public class IpCameraHandler extends BaseThingHandler {
updateImageChannel = false; updateImageChannel = false;
} else { } else {
updateImageChannel = true; updateImageChannel = true;
sendHttpGET(snapshotUri);// Allows this to change Image FPS on demand updateSnapshot();// Allows this to change Image FPS on demand
} }
} else { } else {
Ffmpeg localSnaps = ffmpegSnapshot; Ffmpeg localSnaps = ffmpegSnapshot;
@ -1194,7 +1173,7 @@ public class IpCameraHandler extends BaseThingHandler {
return; return;
} }
onvifCamera.setAbsolutePan(Float.valueOf(command.toString())); onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS); mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
} }
return; return;
case CHANNEL_TILT: case CHANNEL_TILT:
@ -1219,7 +1198,7 @@ public class IpCameraHandler extends BaseThingHandler {
return; return;
} }
onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString())); onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS); mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
} }
return; return;
case CHANNEL_ZOOM: case CHANNEL_ZOOM:
@ -1244,7 +1223,7 @@ public class IpCameraHandler extends BaseThingHandler {
return; return;
} }
onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString())); onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS); mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
} }
return; return;
} }
@ -1316,11 +1295,14 @@ public class IpCameraHandler extends BaseThingHandler {
Future<?> localFuture = cameraConnectionJob; Future<?> localFuture = cameraConnectionJob;
if (localFuture != null) { if (localFuture != null) {
localFuture.cancel(false); localFuture.cancel(false);
cameraConnectionJob = null;
} }
if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) { if (!snapshotUri.isEmpty()) {
snapshotPolling = true; if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000, cameraConfig.getPollTime(), snapshotPolling = true;
TimeUnit.MILLISECONDS); snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000,
cameraConfig.getPollTime(), TimeUnit.MILLISECONDS);
}
} }
pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS); pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
@ -1341,10 +1323,10 @@ public class IpCameraHandler extends BaseThingHandler {
} }
void snapshotIsFfmpeg() { void snapshotIsFfmpeg() {
bringCameraOnline();
snapshotUri = "";// ffmpeg is a valid option. Simplify further checks. snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
logger.debug( logger.debug(
"Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP."); "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
bringCameraOnline();
if (!rtspUri.isEmpty()) { if (!rtspUri.isEmpty()) {
updateImageChannel = false; updateImageChannel = false;
ffmpegSnapshotGeneration = true; ffmpegSnapshotGeneration = true;
@ -1363,7 +1345,7 @@ public class IpCameraHandler extends BaseThingHandler {
if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) { if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
snapshotIsFfmpeg(); snapshotIsFfmpeg();
} else { } else {
sendHttpRequest("GET", snapshotUri, null); updateSnapshot();
} }
return; return;
} }
@ -1375,7 +1357,7 @@ public class IpCameraHandler extends BaseThingHandler {
if ("ffmpeg".equals(snapshotUri)) { if ("ffmpeg".equals(snapshotUri)) {
snapshotIsFfmpeg(); snapshotIsFfmpeg();
} else if (!snapshotUri.isEmpty()) { } else if (!snapshotUri.isEmpty()) {
sendHttpRequest("GET", snapshotUri, null); updateSnapshot();
} else if (!rtspUri.isEmpty()) { } else if (!rtspUri.isEmpty()) {
snapshotIsFfmpeg(); snapshotIsFfmpeg();
} else { } else {
@ -1410,7 +1392,7 @@ public class IpCameraHandler extends BaseThingHandler {
void snapshotRunnable() { void snapshotRunnable() {
// Snapshot should be first to keep consistent time between shots // Snapshot should be first to keep consistent time between shots
sendHttpGET(snapshotUri); updateSnapshot();
if (snapCount > 0) { if (snapCount > 0) {
if (--snapCount == 0) { if (--snapCount == 0) {
setupFfmpegFormat(FFmpegFormat.GIF); setupFfmpegFormat(FFmpegFormat.GIF);
@ -1418,9 +1400,18 @@ public class IpCameraHandler extends BaseThingHandler {
} }
} }
private void takeSnapshot() {
sendHttpGET(snapshotUri);
}
private void updateSnapshot() {
lastSnapshotRequest = Instant.now();
mainEventLoopGroup.schedule(this::takeSnapshot, 0, TimeUnit.MILLISECONDS);
}
public byte[] getSnapshot() { public byte[] getSnapshot() {
if (!isOnline) { if (!isOnline) {
// Keep streams open when the camera goes offline so they dont stop. // Single gray pixel JPG to keep streams open when the camera goes offline so they dont stop.
return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43, 0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43,
0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04, 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
@ -1431,8 +1422,10 @@ public class IpCameraHandler extends BaseThingHandler {
(byte) 0xff, (byte) 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, (byte) 0xff, (byte) 0xda, 0x00, 0x08, (byte) 0xff, (byte) 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, (byte) 0xff, (byte) 0xda, 0x00, 0x08,
0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, (byte) 0xd2, (byte) 0xcf, 0x20, (byte) 0xff, (byte) 0xd9 }; 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, (byte) 0xd2, (byte) 0xcf, 0x20, (byte) 0xff, (byte) 0xd9 };
} }
if (!snapshotPolling && !ffmpegSnapshotGeneration) { // Most cameras will return a 503 busy error if snapshot is faster than 1 second
sendHttpGET(snapshotUri); long lastUpdatedMs = Duration.between(lastSnapshotRequest, Instant.now()).toMillis();
if (!snapshotPolling && !ffmpegSnapshotGeneration && lastUpdatedMs >= cameraConfig.getPollTime()) {
updateSnapshot();
} }
lockCurrentSnapshot.lock(); lockCurrentSnapshot.lock();
try { try {
@ -1464,26 +1457,19 @@ public class IpCameraHandler extends BaseThingHandler {
if (snapshotPolling || ffmpegSnapshotGeneration) { if (snapshotPolling || ffmpegSnapshotGeneration) {
return; // Already polling or creating with FFmpeg from RTSP return; // Already polling or creating with FFmpeg from RTSP
} }
if (streamingSnapshotMjpeg || streamingAutoFps) { if (streamingSnapshotMjpeg || streamingAutoFps || cameraConfig.getUpdateImageWhen().contains("4")) {
snapshotPolling = true; snapshotPolling = true;
snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 200, cameraConfig.getPollTime(), snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 0, cameraConfig.getPollTime(),
TimeUnit.MILLISECONDS);
} else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
snapshotPolling = true;
snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
TimeUnit.MILLISECONDS); TimeUnit.MILLISECONDS);
} }
} }
/** /**
* {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep mjpeg and alarm * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep alarm
* streams open and more. * streams open and more.
* *
*/ */
void pollCameraRunnable() { void pollCameraRunnable() {
if (!snapshotUri.isEmpty() && !snapshotPolling) {// we need to check camera is still online.
checkCameraConnection();
}
// NOTE: Use lowPriorityRequests if get request is not needed every poll. // NOTE: Use lowPriorityRequests if get request is not needed every poll.
if (!lowPriorityRequests.isEmpty()) { if (!lowPriorityRequests.isEmpty()) {
if (lowPriorityCounter >= lowPriorityRequests.size()) { if (lowPriorityCounter >= lowPriorityRequests.size()) {
@ -1494,13 +1480,29 @@ public class IpCameraHandler extends BaseThingHandler {
// what needs to be done every poll// // what needs to be done every poll//
switch (thing.getThingTypeUID().getId()) { switch (thing.getThingTypeUID().getId()) {
case GENERIC_THING: case GENERIC_THING:
if (!snapshotUri.isEmpty() && !snapshotPolling) {
checkCameraConnection();
}
// RTSP stream has stopped and we need it for snapshots
if (ffmpegSnapshotGeneration) {
Ffmpeg localSnapshot = ffmpegSnapshot;
if (localSnapshot != null && !localSnapshot.getIsAlive()) {
localSnapshot.startConverting();
}
}
break; break;
case ONVIF_THING: case ONVIF_THING:
if (!snapshotPolling) {
checkCameraConnection();
}
if (!onvifCamera.isConnected()) { if (!onvifCamera.isConnected()) {
onvifCamera.connect(true); onvifCamera.connect(true);
} }
break; break;
case INSTAR_THING: case INSTAR_THING:
if (!snapshotPolling) {
checkCameraConnection();
}
noMotionDetected(CHANNEL_MOTION_ALARM); noMotionDetected(CHANNEL_MOTION_ALARM);
noMotionDetected(CHANNEL_PIR_ALARM); noMotionDetected(CHANNEL_PIR_ALARM);
noAudioDetected(); noAudioDetected();
@ -1517,6 +1519,9 @@ public class IpCameraHandler extends BaseThingHandler {
sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation"); sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
break; break;
case DAHUA_THING: case DAHUA_THING:
if (!snapshotPolling) {
checkCameraConnection();
}
// Check for alarms, channel for NVRs appears not to work at filtering. // Check for alarms, channel for NVRs appears not to work at filtering.
if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) { if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
logger.info("The alarm stream was not running for camera {}, re-starting it now", logger.info("The alarm stream was not running for camera {}, re-starting it now",
@ -1525,6 +1530,9 @@ public class IpCameraHandler extends BaseThingHandler {
} }
break; break;
case DOORBIRD_THING: case DOORBIRD_THING:
if (!snapshotPolling) {
checkCameraConnection();
}
// Check for alarms, channel for NVRs appears not to work at filtering. // Check for alarms, channel for NVRs appears not to work at filtering.
if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) { if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
logger.info("The alarm stream was not running for camera {}, re-starting it now", logger.info("The alarm stream was not running for camera {}, re-starting it now",
@ -1541,7 +1549,7 @@ public class IpCameraHandler extends BaseThingHandler {
if (localHLS != null) { if (localHLS != null) {
localHLS.checkKeepAlive(); localHLS.checkKeepAlive();
} }
if (openChannels.size() > 18) { if (openChannels.size() > 10) {
logger.debug("There are {} open Channels being tracked.", openChannels.size()); logger.debug("There are {} open Channels being tracked.", openChannels.size());
cleanChannels(); cleanChannels();
} }
@ -1550,7 +1558,7 @@ public class IpCameraHandler extends BaseThingHandler {
@Override @Override
public void initialize() { public void initialize() {
cameraConfig = getConfigAs(CameraConfig.class); cameraConfig = getConfigAs(CameraConfig.class);
threadPool = Executors.newScheduledThreadPool(4); threadPool = Executors.newScheduledThreadPool(2);
mainEventLoopGroup = new NioEventLoopGroup(3); mainEventLoopGroup = new NioEventLoopGroup(3);
snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl()); snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl()); mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());

View File

@ -333,8 +333,6 @@ public class OnvifConnection {
} }
} else if (message.contains("GetEventPropertiesResponse")) { } else if (message.contains("GetEventPropertiesResponse")) {
sendOnvifRequest(requestBuilder(RequestType.CreatePullPointSubscription, eventXAddr)); sendOnvifRequest(requestBuilder(RequestType.CreatePullPointSubscription, eventXAddr));
} else if (message.contains("SubscribeResponse")) {
logger.info("Onvif Subscribe appears to be working for Alarms/Events.");
} else if (message.contains("CreatePullPointSubscriptionResponse")) { } else if (message.contains("CreatePullPointSubscriptionResponse")) {
subscriptionXAddr = removeIPfromUrl(Helper.fetchXML(message, "SubscriptionReference>", "Address>")); subscriptionXAddr = removeIPfromUrl(Helper.fetchXML(message, "SubscriptionReference>", "Address>"));
logger.debug("subscriptionXAddr={}", subscriptionXAddr); logger.debug("subscriptionXAddr={}", subscriptionXAddr);
@ -863,7 +861,7 @@ public class OnvifConnection {
sendOnvifRequest(requestBuilder(RequestType.Unsubscribe, subscriptionXAddr)); sendOnvifRequest(requestBuilder(RequestType.Unsubscribe, subscriptionXAddr));
} }
// give time for the Unsubscribe request to be sent to the camera. // give time for the Unsubscribe request to be sent to the camera.
threadPool.schedule(this::cleanup, 100, TimeUnit.MILLISECONDS); threadPool.schedule(this::cleanup, 50, TimeUnit.MILLISECONDS);
} else { } else {
cleanup(); cleanup();
} }

View File

@ -160,10 +160,12 @@ public class CameraServlet extends IpCameraServlet {
do { do {
try { try {
output.sendSnapshotBasedFrame(handler.getSnapshot()); output.sendSnapshotBasedFrame(handler.getSnapshot());
Thread.sleep(1005); Thread.sleep(handler.cameraConfig.getPollTime());
} catch (InterruptedException | IOException e) { } catch (InterruptedException | IOException e) {
// Never stop streaming until IOException. Occurs when browser stops the stream. // Never stop streaming until IOException. Occurs when browser stops the stream.
openSnapshotStreams.removeStream(output); openSnapshotStreams.removeStream(output);
logger.debug("Now there are {} snapshots.mjpeg streams open.",
openSnapshotStreams.getNumberOfStreams());
if (openSnapshotStreams.isEmpty()) { if (openSnapshotStreams.isEmpty()) {
handler.streamingSnapshotMjpeg = false; handler.streamingSnapshotMjpeg = false;
handler.stopSnapshotPolling(); handler.stopSnapshotPolling();
@ -187,8 +189,9 @@ public class CameraServlet extends IpCameraServlet {
} else { } else {
ChannelTracking tracker = handler.channelTrackingMap.get(handler.mjpegUri); ChannelTracking tracker = handler.channelTrackingMap.get(handler.mjpegUri);
if (tracker == null || !tracker.getChannel().isOpen()) { if (tracker == null || !tracker.getChannel().isOpen()) {
logger.warn("Not the first stream requested but the stream from camera was closed"); logger.debug("Not the first stream requested but the stream from camera was closed");
handler.openCamerasStream(); handler.openCamerasStream();
openStreams.closeAllStreams();
} }
output = new StreamOutput(resp, handler.mjpegContentType); output = new StreamOutput(resp, handler.mjpegContentType);
openStreams.addStream(output); openStreams.addStream(output);
@ -199,6 +202,7 @@ public class CameraServlet extends IpCameraServlet {
} catch (InterruptedException | IOException e) { } catch (InterruptedException | IOException e) {
// Never stop streaming until IOException. Occurs when browser stops the stream. // Never stop streaming until IOException. Occurs when browser stops the stream.
openStreams.removeStream(output); openStreams.removeStream(output);
logger.debug("Now there are {} ipcamera.mjpeg streams open.", openStreams.getNumberOfStreams());
if (openStreams.isEmpty()) { if (openStreams.isEmpty()) {
if (output.isSnapshotBased) { if (output.isSnapshotBased) {
Ffmpeg localMjpeg = handler.ffmpegMjpeg; Ffmpeg localMjpeg = handler.ffmpegMjpeg;
@ -231,6 +235,8 @@ public class CameraServlet extends IpCameraServlet {
} catch (InterruptedException | IOException e) { } catch (InterruptedException | IOException e) {
// Never stop streaming until IOException. Occurs when browser stops the stream. // Never stop streaming until IOException. Occurs when browser stops the stream.
openAutoFpsStreams.removeStream(output); openAutoFpsStreams.removeStream(output);
logger.debug("Now there are {} autofps.mjpeg streams open.",
openAutoFpsStreams.getNumberOfStreams());
if (openAutoFpsStreams.isEmpty()) { if (openAutoFpsStreams.isEmpty()) {
handler.streamingAutoFps = false; handler.streamingAutoFps = false;
logger.debug("All autofps.mjpeg streams have stopped."); logger.debug("All autofps.mjpeg streams have stopped.");

View File

@ -73,7 +73,12 @@ public class StreamOutput {
} }
public void queueFrame(byte[] frame) { public void queueFrame(byte[] frame) {
fifo.add(frame); try {
fifo.add(frame);
} catch (IllegalStateException e) {
fifo.remove();
fifo.add(frame);
}
} }
public void updateContentType(String contentType) { public void updateContentType(String contentType) {

View File

@ -313,6 +313,7 @@
Default is "1000" which is 1 second. Default is "1000" which is 1 second.
</description> </description>
<default>1000</default> <default>1000</default>
<advanced>true</advanced>
</parameter> </parameter>
</config-description> </config-description>
@ -559,6 +560,7 @@
Default is "1000" which is 1 second. Default is "1000" which is 1 second.
</description> </description>
<default>1000</default> <default>1000</default>
<advanced>true</advanced>
</parameter> </parameter>
<parameter name="ptzContinuous" type="boolean" groupName="Settings"> <parameter name="ptzContinuous" type="boolean" groupName="Settings">
@ -859,6 +861,7 @@
Default is "1000" which is 1 second. Default is "1000" which is 1 second.
</description> </description>
<default>1000</default> <default>1000</default>
<advanced>true</advanced>
</parameter> </parameter>
</config-description> </config-description>
@ -1149,6 +1152,7 @@
Default is "1000" which is 1 second. Default is "1000" which is 1 second.
</description> </description>
<default>1000</default> <default>1000</default>
<advanced>true</advanced>
</parameter> </parameter>
</config-description> </config-description>
@ -1408,6 +1412,7 @@
Default is "1000" which is 1 second. Default is "1000" which is 1 second.
</description> </description>
<default>1000</default> <default>1000</default>
<advanced>true</advanced>
</parameter> </parameter>
</config-description> </config-description>
@ -1685,6 +1690,7 @@
Default is "1000" which is 1 second. Default is "1000" which is 1 second.
</description> </description>
<default>1000</default> <default>1000</default>
<advanced>true</advanced>
</parameter> </parameter>
</config-description> </config-description>
@ -1969,6 +1975,7 @@
Default is "1000" which is 1 second. Default is "1000" which is 1 second.
</description> </description>
<default>1000</default> <default>1000</default>
<advanced>true</advanced>
</parameter> </parameter>
</config-description> </config-description>
@ -2237,6 +2244,7 @@
Default is "1000" which is 1 second. Default is "1000" which is 1 second.
</description> </description>
<default>1000</default> <default>1000</default>
<advanced>true</advanced>
</parameter> </parameter>
</config-description> </config-description>
</thing-type> </thing-type>