mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-10-03 16:05:24 -04:00
Monitor::Pause() reset shared_data->last_write_index to image_buffer_count, the "nothing written yet" sentinel. Once an OnDemand monitor went to sleep the last captured image became unreachable, so zms mode=single gave up waiting and returned "No image available." mode=jpeg worked only because runStream calls setLastViewed() each iteration, waking capture for a fresh frame. The zmc OnDemand loop also paused before capturing anything: on a fresh shm last_viewed is 0, so the first iteration paused a primed camera and no initial image was ever written for the console thumbnail. The GetLastWriteIndex() guard that prevented this had been removed because Pause() clobbering the index made it cycle Pause/Play. Leave last_write_index alone in Pause() and restore the guard so capture continues until one image has been written. The index then stays valid, so the pause is stable and mode=single serves the last captured image. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
433 lines
14 KiB
C++
433 lines
14 KiB
C++
//
|
|
// ZoneMinder Capture Daemon, $Date$, $Revision$
|
|
// Copyright (C) 2001-2008 Philip Coombes
|
|
//
|
|
// This program is free software; you can redistribute it and/or
|
|
// modify it under the terms of the GNU General Public License
|
|
// as published by the Free Software Foundation; either version 2
|
|
// of the License, or (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program; if not, write to the Free Software
|
|
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
//
|
|
|
|
/*
|
|
|
|
=head1 NAME
|
|
|
|
zmc - The ZoneMinder Capture daemon
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
zmc -d <device_path>
|
|
zmc --device <device_path>
|
|
zmc -f <file_path>
|
|
zmc --file <file_path>
|
|
zmc -m <monitor_id>
|
|
zmc --monitor <monitor_id>
|
|
zmc -h
|
|
zmc --help
|
|
zmc -v
|
|
zmc --version
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
This binary's job is to sit on a video device and suck frames off it as fast as
|
|
possible, this should run at more or less constant speed.
|
|
|
|
=head1 OPTIONS
|
|
|
|
-d, --device <device_path> - For local cameras, device to access. e.g /dev/video0 etc
|
|
-f, --file <file_path> - For local images, jpg file to access.
|
|
-m, --monitor_id - ID of the monitor to analyse
|
|
-h, --help - Display usage information
|
|
-v, --version - Print the installed version of ZoneMinder
|
|
|
|
=cut
|
|
|
|
*/
|
|
|
|
#include "zm.h"
|
|
#include "zm_camera.h"
|
|
#include "zm_db.h"
|
|
#include "zm_define.h"
|
|
#include "zm_fifo.h"
|
|
#include "zm_monitor.h"
|
|
#include "zm_signal.h"
|
|
#include "zm_time.h"
|
|
#include "zm_utils.h"
|
|
|
|
#include <getopt.h>
|
|
#include <iostream>
|
|
#include <unistd.h>
|
|
|
|
void Usage() {
|
|
fprintf(stderr, "zmc -d <device_path> or -r <proto> -H <host> -P <port> -p <path> or -f <file_path> or -m <monitor_id>\n");
|
|
|
|
fprintf(stderr, "Options:\n");
|
|
#if defined(BSD)
|
|
fprintf(stderr, " -d, --device <device_path> : For local cameras, device to access. E.g /dev/bktr0 etc\n");
|
|
#else
|
|
fprintf(stderr, " -d, --device <device_path> : For local cameras, device to access. E.g /dev/video0 etc\n");
|
|
#endif
|
|
fprintf(stderr, " -f, --file <file_path> : For local images, jpg file to access.\n");
|
|
fprintf(stderr, " -m, --monitor <monitor_id> : For sources associated with a single monitor\n");
|
|
fprintf(stderr, " -h, --help : This screen\n");
|
|
fprintf(stderr, " -v, --version : Report the installed version of ZoneMinder\n");
|
|
exit(0);
|
|
}
|
|
|
|
int main(int argc, char *argv[]) {
|
|
self = argv[0];
|
|
|
|
srand(getpid() * time(nullptr));
|
|
|
|
const char *device = "";
|
|
const char *protocol = "";
|
|
const char *host = "";
|
|
const char *port = "";
|
|
const char *path = "";
|
|
const char *file = "";
|
|
int monitor_id = -1;
|
|
|
|
static struct option long_options[] = {
|
|
{"device", 1, nullptr, 'd'},
|
|
{"protocol", 1, nullptr, 'r'},
|
|
{"host", 1, nullptr, 'H'},
|
|
{"port", 1, nullptr, 'P'},
|
|
{"path", 1, nullptr, 'p'},
|
|
{"file", 1, nullptr, 'f'},
|
|
{"monitor", 1, nullptr, 'm'},
|
|
{"help", 0, nullptr, 'h'},
|
|
{"version", 0, nullptr, 'v'},
|
|
{nullptr, 0, nullptr, 0}
|
|
};
|
|
|
|
while (1) {
|
|
int option_index = 0;
|
|
|
|
int c = getopt_long(argc, argv, "d:H:P:p:f:m:h:v", long_options, &option_index);
|
|
if ( c == -1 ) {
|
|
break;
|
|
}
|
|
|
|
switch (c) {
|
|
case 'd':
|
|
device = optarg;
|
|
break;
|
|
case 'H':
|
|
host = optarg;
|
|
break;
|
|
case 'P':
|
|
port = optarg;
|
|
break;
|
|
case 'p':
|
|
path = optarg;
|
|
break;
|
|
case 'f':
|
|
file = optarg;
|
|
break;
|
|
case 'm':
|
|
monitor_id = atoi(optarg);
|
|
break;
|
|
case 'h':
|
|
case '?':
|
|
Usage();
|
|
break;
|
|
case 'v':
|
|
std::cout << ZM_VERSION << "\n";
|
|
exit(0);
|
|
default:
|
|
// fprintf(stderr, "?? getopt returned character code 0%o ??\n", c);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( optind < argc ) {
|
|
fprintf(stderr, "Extraneous options, ");
|
|
while ( optind < argc )
|
|
printf("%s ", argv[optind++]);
|
|
printf("\n");
|
|
Usage();
|
|
}
|
|
|
|
int modes = ( (device[0]?1:0) + (host[0]?1:0) + (file[0]?1:0) + (monitor_id > 0 ? 1 : 0) );
|
|
if ( modes > 1 ) {
|
|
fprintf(stderr, "Only one of device, host/port/path, file or monitor id allowed\n");
|
|
Usage();
|
|
exit(0);
|
|
}
|
|
|
|
if ( modes < 1 ) {
|
|
fprintf(stderr, "One of device, host/port/path, file or monitor id must be specified\n");
|
|
Usage();
|
|
exit(0);
|
|
}
|
|
|
|
char log_id_string[32] = "";
|
|
if ( device[0] ) {
|
|
const char *slash_ptr = strrchr(device, '/');
|
|
snprintf(log_id_string, sizeof(log_id_string), "zmc_d%s", slash_ptr?slash_ptr+1:device);
|
|
} else if ( host[0] ) {
|
|
snprintf(log_id_string, sizeof(log_id_string), "zmc_h%s", host);
|
|
} else if ( file[0] ) {
|
|
const char *slash_ptr = strrchr(file, '/');
|
|
snprintf(log_id_string, sizeof(log_id_string), "zmc_f%s", slash_ptr?slash_ptr+1:file);
|
|
} else {
|
|
snprintf(log_id_string, sizeof(log_id_string), "zmc_m%d", monitor_id);
|
|
}
|
|
|
|
logInit(log_id_string);
|
|
zmLoadStaticConfig();
|
|
zmDbConnect();
|
|
zmLoadDBConfig();
|
|
logInit(log_id_string);
|
|
|
|
HwCapsDetect();
|
|
#if HAVE_LIBCURL
|
|
curl_global_init(CURL_GLOBAL_DEFAULT);
|
|
#endif // HAVE_LIBCURL
|
|
|
|
std::vector<std::shared_ptr<Monitor>> monitors;
|
|
#if ZM_HAS_V4L2
|
|
if ( device[0] ) {
|
|
monitors = Monitor::LoadLocalMonitors(device, Monitor::CAPTURE);
|
|
} else
|
|
#endif // ZM_HAS_V4L2
|
|
if ( host[0] ) {
|
|
if ( !port )
|
|
port = "80";
|
|
monitors = Monitor::LoadRemoteMonitors(protocol, host, port, path, Monitor::CAPTURE);
|
|
} else if ( file[0] ) {
|
|
monitors = Monitor::LoadFileMonitors(file, Monitor::CAPTURE);
|
|
} else {
|
|
std::shared_ptr<Monitor> monitor = Monitor::Load(monitor_id, true, Monitor::CAPTURE);
|
|
if ( monitor ) {
|
|
monitors.push_back(monitor);
|
|
}
|
|
}
|
|
|
|
if (monitors.empty()) {
|
|
Error("No monitors found");
|
|
exit(-1);
|
|
} else {
|
|
Debug(2, "%zu monitors loaded", monitors.size());
|
|
}
|
|
|
|
Info("Starting Capture version %s", ZM_VERSION);
|
|
zmSetDefaultHupHandler();
|
|
zmSetDefaultTermHandler();
|
|
zmSetDefaultDieHandler();
|
|
|
|
struct sigaction sa;
|
|
sa.sa_handler = SIG_IGN; //handle signal by ignoring
|
|
sigemptyset(&sa.sa_mask);
|
|
sa.sa_flags = 0;
|
|
if (sigaction(SIGCHLD, &sa, 0) == -1) {
|
|
Error("Unable to set SIGCHLD to ignore. There may be zombies.");
|
|
}
|
|
|
|
int result = 0;
|
|
int prime_capture_log_count = 0;
|
|
|
|
while (!zm_terminate) {
|
|
result = 0;
|
|
|
|
for (const std::shared_ptr<Monitor> &monitor : monitors) {
|
|
std::string sql = stringtf(
|
|
"INSERT INTO Monitor_Status (MonitorId,Status,CaptureFPS,AnalysisFPS,CaptureBandwidth)"
|
|
" VALUES (%u, 'Running',0,0,0) ON DUPLICATE KEY UPDATE Status='Running',CaptureFPS=0,AnalysisFPS=0,CaptureBandwidth=0",
|
|
monitor->Id());
|
|
zmDbDo(sql);
|
|
|
|
monitor->LoadCamera();
|
|
|
|
while (!monitor->connect() and !zm_terminate) {
|
|
Warning("Couldn't connect to monitor %d", monitor->Id());
|
|
monitor->SetHeartbeatTime(std::chrono::system_clock::now());
|
|
sleep(1);
|
|
}
|
|
if (zm_terminate) break;
|
|
|
|
SystemTimePoint now = std::chrono::system_clock::now();
|
|
monitor->SetStartupTime(now);
|
|
|
|
if (monitor->StartupDelay() > 0) {
|
|
Debug(1, "Doing startup sleep for %ds", monitor->StartupDelay());
|
|
std::this_thread::sleep_for(Seconds(monitor->StartupDelay()));
|
|
}
|
|
|
|
Seconds sleep_time = Seconds(0);
|
|
while ((monitor->PrimeCapture() <= 0) and !zm_terminate) {
|
|
if (prime_capture_log_count % 60) {
|
|
logPrintf(Logger::ERROR + monitor->Importance(), "Failed to prime capture of initial monitor");
|
|
} else {
|
|
Debug(1, "Failed to prime capture of initial monitor");
|
|
}
|
|
|
|
prime_capture_log_count++;
|
|
if (sleep_time < Seconds(static_cast<int>(config.watch_max_delay))) {
|
|
sleep_time++;
|
|
}
|
|
|
|
std::this_thread::sleep_for(sleep_time);
|
|
monitor->SetHeartbeatTime(std::chrono::system_clock::now());
|
|
}
|
|
if (zm_terminate) break;
|
|
|
|
sql = stringtf(
|
|
"INSERT INTO Monitor_Status (MonitorId,Status) VALUES (%u, 'Connected') ON DUPLICATE KEY UPDATE Status='Connected'",
|
|
monitor->Id());
|
|
zmDbDo(sql);
|
|
} // end foreach monitor
|
|
|
|
if (zm_terminate) break;
|
|
|
|
std::vector<SystemTimePoint> last_capture_times = std::vector<SystemTimePoint>(monitors.size());
|
|
Microseconds sleep_time = Microseconds(0);
|
|
|
|
while (!zm_terminate) {
|
|
for (size_t i = 0; i < monitors.size(); i++) {
|
|
monitors[i]->CheckAction();
|
|
|
|
if (monitors[i]->Capturing() == Monitor::CAPTURING_ONDEMAND) {
|
|
SystemTimePoint now = std::chrono::system_clock::now();
|
|
monitors[i]->SetHeartbeatTime(now);
|
|
|
|
time_t last_viewed = monitors[i]->getLastViewed();
|
|
int64 since_last_view = static_cast<int64>(std::chrono::duration_cast<Seconds>(now.time_since_epoch()).count()) - last_viewed;
|
|
Debug(1, "Last view %jd= %" PRId64 " seconds since last view", last_viewed, since_last_view);
|
|
if (!last_viewed or (since_last_view > 10)) {
|
|
// Nobody is watching — pause if running, otherwise stay paused.
|
|
// Keep capturing until we have written at least one image so that
|
|
// the console thumbnail and mode=single have something to show.
|
|
// Pause() no longer resets last_write_index, so once we have an
|
|
// image this stays true and we don't Pause/Play cycle.
|
|
if (monitors[i]->GetLastWriteIndex() != -1) {
|
|
if (monitors[i]->getCamera()->isPrimed()) {
|
|
monitors[i]->Pause();
|
|
}
|
|
std::this_thread::sleep_for(Microseconds(100000));
|
|
result = 0;
|
|
continue;
|
|
}
|
|
} else if (!monitors[i]->getCamera()->isPrimed()) {
|
|
if (1 > (result = monitors[i]->Play())) {
|
|
Debug(1, "Failed to play");
|
|
break;
|
|
}
|
|
}
|
|
} // end if ONDEMAND
|
|
|
|
if (monitors[i]->PreCapture() < 0) {
|
|
Error("Failed to pre-capture monitor %d %s (%zu/%zu)",
|
|
monitors[i]->Id(), monitors[i]->Name(), i + 1, monitors.size());
|
|
result = -1;
|
|
break;
|
|
}
|
|
if (monitors[i]->Capture() < 0) {
|
|
if (!zm_terminate)
|
|
logPrintf(Logger::ERROR + monitors[i]->Importance(), "Failed to capture image from monitor %d %s (%zu/%zu)",
|
|
monitors[i]->Id(), monitors[i]->Name(), i + 1, monitors.size());
|
|
result = -1;
|
|
break;
|
|
}
|
|
if (monitors[i]->PostCapture() < 0) {
|
|
Error("Failed to post-capture monitor %d %s (%zu/%zu)",
|
|
monitors[i]->Id(), monitors[i]->Name(), i + 1, monitors.size());
|
|
result = -1;
|
|
break;
|
|
}
|
|
|
|
if (!result) monitors[i]->UpdateFPS();
|
|
|
|
SystemTimePoint now = std::chrono::system_clock::now();
|
|
monitors[i]->SetHeartbeatTime(now);
|
|
|
|
// capture_delay is the amount of time we should sleep in useconds to achieve the desired framerate.
|
|
Microseconds delay = (monitors[i]->GetState() == Monitor::ALARM) ? monitors[i]->GetAlarmCaptureDelay()
|
|
: monitors[i]->GetCaptureDelay();
|
|
if (delay != Microseconds(0)) {
|
|
if (last_capture_times[i].time_since_epoch() != Seconds(0)) {
|
|
Microseconds delta_time = std::chrono::duration_cast<Microseconds>(now - last_capture_times[i]);
|
|
|
|
// You have to add back in the previous sleep time
|
|
sleep_time = delay - (delta_time - sleep_time);
|
|
|
|
// Limit negative since feedback - if an error occurred can loose seconds and then run too fast for a while so rather accept the frame loss
|
|
// Or if the camera exposure is longer than intended FPS (eg at night) don't build up a huge negative that will disable the FPS limit later and OOM (eg at dawn)
|
|
if (sleep_time < (delay * -1)) {
|
|
sleep_time = delay * -1;
|
|
}
|
|
|
|
Debug(4,
|
|
"Sleep time is %" PRIi64 " us from now: %.3f s last: %.3f s delta % " PRIi64 " us delay: %" PRIi64 " us",
|
|
static_cast<int64>(Microseconds(sleep_time).count()),
|
|
FPSeconds(now.time_since_epoch()).count(),
|
|
FPSeconds(last_capture_times[i].time_since_epoch()).count(),
|
|
static_cast<int64>(delta_time.count()),
|
|
static_cast<int64>(Microseconds(delay).count()));
|
|
|
|
// When negative the capture took more than the configured FPS time and no need to sleep
|
|
if (sleep_time > Microseconds(0)) {
|
|
std::this_thread::sleep_for(sleep_time);
|
|
}
|
|
} // end if has a last_capture time
|
|
|
|
last_capture_times[i] = now;
|
|
} // end if delay
|
|
} // end foreach n_monitors
|
|
|
|
if ((result < 0) or zm_reload) {
|
|
// Failure, try reconnecting
|
|
break;
|
|
}
|
|
} // end while ! zm_terminate and connected
|
|
|
|
for (std::shared_ptr<Monitor> & monitor : monitors) {
|
|
monitor->SetHeartbeatTime(std::chrono::system_clock::now());
|
|
monitor->Close();
|
|
monitor->SetHeartbeatTime(std::chrono::system_clock::now());
|
|
monitor->disconnect();
|
|
}
|
|
|
|
if (zm_reload) {
|
|
zmLoadStaticConfig();
|
|
zmLoadDBConfig();
|
|
for (std::shared_ptr<Monitor> &monitor : monitors) {
|
|
monitor->Reload();
|
|
}
|
|
logTerm();
|
|
logInit(log_id_string);
|
|
|
|
zm_reload = false;
|
|
} // end if zm_reload
|
|
} // end while ! zm_terminate outer connection loop
|
|
|
|
for (std::shared_ptr<Monitor> &monitor : monitors) {
|
|
std::string sql = stringtf(
|
|
"INSERT INTO Monitor_Status (MonitorId,Status) VALUES (%u, 'NotRunning') ON DUPLICATE KEY UPDATE Status='NotRunning',CaptureFPS=0,AnalysisFPS=0,CaptureBandwidth=0",
|
|
monitor->Id());
|
|
zmDbDo(sql);
|
|
}
|
|
monitors.clear();
|
|
Debug(1, "Cleared monitors");
|
|
|
|
Image::Deinitialise();
|
|
#if HAVE_LIBCURL
|
|
curl_global_cleanup();
|
|
#endif // HAVE_LIBCURL
|
|
Debug(1, "terminating");
|
|
dbQueue.stop();
|
|
zmDbClose();
|
|
logTerm();
|
|
|
|
return zm_terminate ? 0 : result;
|
|
}
|