import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URL;
+import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
public class IpCameraHandler extends BaseThingHandler {
public final Logger logger = LoggerFactory.getLogger(getClass());
public final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
- private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(4);
+ private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2);
private GroupTracker groupTracker;
public CameraConfig cameraConfig = new CameraConfig();
public @Nullable Ffmpeg ffmpegSnapshot = null;
public boolean streamingAutoFps = false;
public boolean motionDetected = false;
+ public Instant lastSnapshotRequest = Instant.now();
public Instant currentSnapshotTime = Instant.now();
private @Nullable ScheduledFuture<?> cameraConnectionJob = null;
private @Nullable ScheduledFuture<?> pollCameraJob = null;
private String boundary = "";
private Object reply = new Object();
private String requestUrl = "";
- private boolean closeConnection = true;
private boolean isChunked = false;
public void setURL(String url) {
case "content-length":
bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
break;
- case "connection":
- if (response.headers().getAsString(name).contains("keep-alive")) {
- closeConnection = false;
- }
- break;
case "transfer-encoding":
if (response.headers().getAsString(name).contains("chunked")) {
isChunked = true;
}
}
if (contentType.contains("multipart")) {
- closeConnection = false;
if (mjpegUri.equals(requestUrl)) {
if (msg instanceof HttpMessage) {
// very start of stream only
}
if (content instanceof LastHttpContent) {
processSnapshot(incomingJpeg);
- // testing next line and if works need to do a full cleanup of this function.
- closeConnection = true;
- if (closeConnection) {
- ctx.close();
- } else {
- bytesToRecieve = 0;
- bytesAlreadyRecieved = 0;
- }
+ ctx.close();
}
} else { // incomingMessage that is not an IMAGE
if (incomingMessage.isEmpty()) {
}
}
- @Override
- public void channelReadComplete(@Nullable ChannelHandlerContext ctx) {
- }
-
- @Override
- public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
- }
-
- @Override
- public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
- }
-
@Override
public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
if (cause == null || ctx == null) {
case DAHUA_THING:
urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
break;
- case HIKVISION_THING:
- urlToKeepOpen = "/ISAPI/Event/notification/alertStream";
- break;
case DOORBIRD_THING:
urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
break;
return; // don't auto close this as it is for the alarms.
}
}
+ logger.debug("Closing an idle channel for camera:{}", cameraConfig.getIp());
ctx.close();
}
}
}
private void checkCameraConnection() {
+ if (snapshotUri.isEmpty() || snapshotPolling) {
+ // Already polling or camera has RTSP only and no HTTP server
+ return;
+ }
Bootstrap localBootstrap = mainBootstrap;
if (localBootstrap != null) {
ChannelFuture chFuture = localBootstrap
}
public void openCamerasStream() {
- threadPool.schedule(this::openMjpegStream, 500, TimeUnit.MILLISECONDS);
+ closeChannel(getTinyUrl(mjpegUri));
+ mainEventLoopGroup.schedule(this::openMjpegStream, 0, TimeUnit.MILLISECONDS);
}
private void openMjpegStream() {
* open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
* still occurring.
*/
- void cleanChannels() {
+ private void cleanChannels() {
for (Channel channel : openChannels) {
boolean oldChannel = true;
for (ChannelTracking channelTracking : channelTrackingMap.values()) {
channelTrackingMap.remove(channelTracking.getRequestUrl());
}
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;
}
}
if (cameraConfig.getUpdateImageWhen().contains("2")) {
if (!firstMotionAlarm) {
if (!snapshotUri.isEmpty()) {
- sendHttpGET(snapshotUri);
+ updateSnapshot();
}
firstMotionAlarm = true;// reset back to false when the jpg arrives.
}
if (cameraConfig.getUpdateImageWhen().contains("3")) {
if (!firstAudioAlarm) {
if (!snapshotUri.isEmpty()) {
- sendHttpGET(snapshotUri);
+ updateSnapshot();
}
firstAudioAlarm = true;// reset back to false when the jpg arrives.
}
updateImageChannel = false;
} else {
updateImageChannel = true;
- sendHttpGET(snapshotUri);// Allows this to change Image FPS on demand
+ updateSnapshot();// Allows this to change Image FPS on demand
}
} else {
Ffmpeg localSnaps = ffmpegSnapshot;
return;
}
onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
- threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
+ mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
}
return;
case CHANNEL_TILT:
return;
}
onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
- threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
+ mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
}
return;
case CHANNEL_ZOOM:
return;
}
onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
- threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
+ mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
}
return;
}
Future<?> localFuture = cameraConnectionJob;
if (localFuture != null) {
localFuture.cancel(false);
+ cameraConnectionJob = null;
}
- if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
- snapshotPolling = true;
- snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000, cameraConfig.getPollTime(),
- TimeUnit.MILLISECONDS);
+ if (!snapshotUri.isEmpty()) {
+ if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
+ snapshotPolling = true;
+ snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000,
+ cameraConfig.getPollTime(), TimeUnit.MILLISECONDS);
+ }
}
pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
}
void snapshotIsFfmpeg() {
- bringCameraOnline();
snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
logger.debug(
"Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
+ bringCameraOnline();
if (!rtspUri.isEmpty()) {
updateImageChannel = false;
ffmpegSnapshotGeneration = true;
if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
snapshotIsFfmpeg();
} else {
- sendHttpRequest("GET", snapshotUri, null);
+ updateSnapshot();
}
return;
}
if ("ffmpeg".equals(snapshotUri)) {
snapshotIsFfmpeg();
} else if (!snapshotUri.isEmpty()) {
- sendHttpRequest("GET", snapshotUri, null);
+ updateSnapshot();
} else if (!rtspUri.isEmpty()) {
snapshotIsFfmpeg();
} else {
void snapshotRunnable() {
// Snapshot should be first to keep consistent time between shots
- sendHttpGET(snapshotUri);
+ updateSnapshot();
if (snapCount > 0) {
if (--snapCount == 0) {
setupFfmpegFormat(FFmpegFormat.GIF);
}
}
+ private void takeSnapshot() {
+ sendHttpGET(snapshotUri);
+ }
+
+ private void updateSnapshot() {
+ lastSnapshotRequest = Instant.now();
+ mainEventLoopGroup.schedule(this::takeSnapshot, 0, TimeUnit.MILLISECONDS);
+ }
+
public byte[] getSnapshot() {
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,
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,
(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 };
}
- if (!snapshotPolling && !ffmpegSnapshotGeneration) {
- sendHttpGET(snapshotUri);
+ // Most cameras will return a 503 busy error if snapshot is faster than 1 second
+ long lastUpdatedMs = Duration.between(lastSnapshotRequest, Instant.now()).toMillis();
+ if (!snapshotPolling && !ffmpegSnapshotGeneration && lastUpdatedMs >= cameraConfig.getPollTime()) {
+ updateSnapshot();
}
lockCurrentSnapshot.lock();
try {
if (snapshotPolling || ffmpegSnapshotGeneration) {
return; // Already polling or creating with FFmpeg from RTSP
}
- if (streamingSnapshotMjpeg || streamingAutoFps) {
+ if (streamingSnapshotMjpeg || streamingAutoFps || cameraConfig.getUpdateImageWhen().contains("4")) {
snapshotPolling = true;
- snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
- TimeUnit.MILLISECONDS);
- } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
- snapshotPolling = true;
- snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
+ snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 0, cameraConfig.getPollTime(),
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.
*
*/
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.
if (!lowPriorityRequests.isEmpty()) {
if (lowPriorityCounter >= lowPriorityRequests.size()) {
// what needs to be done every poll//
switch (thing.getThingTypeUID().getId()) {
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;
case ONVIF_THING:
+ if (!snapshotPolling) {
+ checkCameraConnection();
+ }
if (!onvifCamera.isConnected()) {
onvifCamera.connect(true);
}
break;
case INSTAR_THING:
+ if (!snapshotPolling) {
+ checkCameraConnection();
+ }
noMotionDetected(CHANNEL_MOTION_ALARM);
noMotionDetected(CHANNEL_PIR_ALARM);
noAudioDetected();
sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
break;
case DAHUA_THING:
+ if (!snapshotPolling) {
+ checkCameraConnection();
+ }
// Check for alarms, channel for NVRs appears not to work at filtering.
if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
logger.info("The alarm stream was not running for camera {}, re-starting it now",
}
break;
case DOORBIRD_THING:
+ if (!snapshotPolling) {
+ checkCameraConnection();
+ }
// Check for alarms, channel for NVRs appears not to work at filtering.
if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
logger.info("The alarm stream was not running for camera {}, re-starting it now",
if (localHLS != null) {
localHLS.checkKeepAlive();
}
- if (openChannels.size() > 18) {
+ if (openChannels.size() > 10) {
logger.debug("There are {} open Channels being tracked.", openChannels.size());
cleanChannels();
}
@Override
public void initialize() {
cameraConfig = getConfigAs(CameraConfig.class);
- threadPool = Executors.newScheduledThreadPool(4);
+ threadPool = Executors.newScheduledThreadPool(2);
mainEventLoopGroup = new NioEventLoopGroup(3);
snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());