Compare commits

..

5 Commits

Author SHA1 Message Date
Maxim Devaev
11fd828ab0 lint fix 2026-11-04 18:35:04 +02:00
Maxim Devaev
5e2542420f add timeout argument for --open-on-demand 2026-11-04 18:34:36 +02:00
Maxim Devaev
b8a224a5fa Merge pull request #343 from lagnat/open-on-demand
Add --open-on-demand: open the capture device only when someone is watching
2026-11-04 16:59:06 +02:00
Maxim Devaev
296639a2ac updated donate links 2026-10-10 07:50:30 +03:00
Stephen Green
7cde878f02 --open-on-demand: open the capture device only when someone is watching
With this option the device is not opened until at least one stream,
snapshot or sink client appears, and it's closed again 3 seconds after
the last one disconnects. While the device is closed, the clients get
the usual blank screen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 12:22:54 -04:00
6 changed files with 50 additions and 10 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,4 +1,4 @@
# These are supported funding model platforms
patreon: pikvm
custom: https://paypal.me/pikvm
ko_fi: pikvm

View File

@@ -112,6 +112,9 @@ It doesn't do anything. Still here for compatibility.
.BR \-l ", " \-\-slowdown
Slowdown capturing to 1 FPS or less when no stream or sink clients are connected. Useful to reduce CPU consumption. Default: disabled.
.TP
.BR \-\-open\-on\-demand \fIsec
Don't open the capture device until at least one stream or sink client is connected, and close it again after a specified timeout since the last one disconnects. Clients see a blank screen while the device is closed. Default: disabled.
.TP
.BR \-\-device\-timeout\ \fIsec
Timeout for device querying. Default: 1.
.TP

View File

@@ -120,8 +120,8 @@ char *us_simplify_request_path(const char *str) {
#ifdef TEST_HTTP_PATH
int test_simplify_request_path(const char *sample, const char *expected) {
char *result = us_simplify_request_path(sample);
int retval = -!!strcmp(result, expected);
const char *result = us_simplify_request_path(sample);
const int retval = -(!!strcmp(result, expected));
printf("Testing '%s' -> '%s' ... ", sample, expected);
if (retval == 0) {

View File

@@ -89,7 +89,8 @@ enum _US_OPT_VALUES {
// Longs only
_O_DEVICE_TIMEOUT = 10000,
_O_OPEN_ON_DEMAND = 10000,
_O_DEVICE_TIMEOUT,
_O_DEVICE_ERROR_DELAY,
_O_FORMAT_SWAP_RGB,
_O_M2M_DEVICE,
@@ -190,6 +191,7 @@ static const struct option _LONG_OPTS[] = {
{"blank", required_argument, NULL, _O_BLANK},
{"last-as-blank", required_argument, NULL, _O_LAST_AS_BLANK},
{"slowdown", no_argument, NULL, _O_SLOWDOWN},
{"open-on-demand", required_argument, NULL, _O_OPEN_ON_DEMAND},
{"device-timeout", required_argument, NULL, _O_DEVICE_TIMEOUT},
{"device-error-delay", required_argument, NULL, _O_DEVICE_ERROR_DELAY},
{"m2m-device", required_argument, NULL, _O_M2M_DEVICE},
@@ -453,6 +455,7 @@ int us_options_parse(
case _O_BLANK: break; // Deprecated
case _O_LAST_AS_BLANK: break; // Deprecated
case _O_SLOWDOWN: OPT_SET(stream->slowdown, true);
case _O_OPEN_ON_DEMAND: OPT_NUMBER("--open-on-demand", stream->open_on_demand, 1, 60, 0);
case _O_DEVICE_TIMEOUT: OPT_NUMBER("--device-timeout", cap->timeout, 1, 60, 0);
case _O_DEVICE_ERROR_DELAY: OPT_NUMBER("--device-error-delay", stream->error_delay, 1, 60, 0);
case _O_M2M_DEVICE: OPT_SET(enc->m2m_path, optarg);
@@ -749,6 +752,10 @@ static void _help(
SAY(" -K|--last-as-blank <sec> ──────────── It doesn't do anything. Still here for compatibility.\n");
SAY(" -l|--slowdown ─────────────────────── Slowdown capturing to 1 FPS or less when no stream or sink clients");
SAY(" are connected. Useful to reduce CPU consumption. Default: disabled.\n");
SAY(" --open-on-demand <sec> ────────────── Don't open the capture device until at least one stream");
SAY(" or sink client is connected, and close it again");
SAY(" after a specified timeout since the last one disconnects.");
SAY(" Clients see a blank screen while the device is closed. Default: disabled.\n");
SAY(" --device-timeout <sec> ────────────── Timeout for device querying. Default: %u.\n", cap->timeout);
SAY(" --device-error-delay <sec> ────────── Delay before trying to connect to the device again");
SAY(" after an error (timeout for example). Default: %u.\n", stream->error_delay);

View File

@@ -206,7 +206,23 @@ void us_stream_loop(us_stream_s *stream) {
US_LOG_INFO("Capturing ...");
int has_any_clients_prev = -1;
ldf no_clients_since_ts = 0;
while (!atomic_load(&run->stop) && !atomic_load(&threads_stop)) {
if (stream->open_on_demand > 0) {
if (_stream_has_any_clients_cached(stream)) {
no_clients_since_ts = 0;
} else {
const ldf now_ts = us_get_now_monotonic();
if (no_clients_since_ts == 0) {
no_clients_since_ts = now_ts;
} else if (no_clients_since_ts + stream->open_on_demand < now_ts) {
US_LOG_INFO("No stream or sink clients found in last %u seconds, closing the device ...",
stream->open_on_demand);
goto close;
}
}
}
us_capture_hwbuf_s *hw;
switch (us_capture_hwbuf_grab(cap, &hw)) {
case 0 ... INT_MAX: break; // Grabbed buffer number
@@ -578,7 +594,18 @@ static int _stream_init_loop(us_stream_s *stream) {
UPDATE_SINK(stream->h264_sink);
# undef UPDATE_SINK
_stream_check_suicide(stream, _stream_has_any_clients_cached(stream));
const bool has_any_clients = _stream_has_any_clients_cached(stream);
_stream_check_suicide(stream, has_any_clients);
if (stream->open_on_demand > 0 && !has_any_clients) {
US_ONCE({ US_LOG_INFO("Waiting for the stream or sink clients to open the device ..."); });
blank_reason = (
"< WAITING FOR CLIENTS >\n \n"
" The camera is idle "
);
goto silent_error;
}
stream->cap->dma_export = (
stream->enc->type == US_ENCODER_TYPE_M2M_VIDEO
@@ -598,7 +625,7 @@ static int _stream_init_loop(us_stream_s *stream) {
" - Bad config \n \n"
" - Malfunction "
);
goto silent_error;
goto silent_capture_error;
case US_ERROR_NO_CABLE:
blank_reason = (
"< NO VIDEO SOURCE >\n \n"
@@ -606,7 +633,7 @@ static int _stream_init_loop(us_stream_s *stream) {
" - Source is off \n \n"
" - Cable problems "
);
goto silent_error;
goto silent_capture_error;
case US_ERROR_NO_SIGNAL:
blank_reason = (
"< NO SIGNAL DETECTED >\n \n"
@@ -614,7 +641,7 @@ static int _stream_init_loop(us_stream_s *stream) {
" - Video suspended \n \n"
" - Cable problems "
);
goto silent_error;
goto silent_capture_error;
case US_ERROR_NO_SYNC:
blank_reason = (
"< NO SYNC WITH SIGNAL >\n \n"
@@ -622,7 +649,7 @@ static int _stream_init_loop(us_stream_s *stream) {
" - Source is crazy \n \n"
" - Cable problems "
);
goto silent_error;
goto silent_capture_error;
case US_ERROR_NO_LANES:
blank_reason = (
"< UNSUPPORTED SIGNAL TIMINGS >\n \n"
@@ -638,10 +665,12 @@ static int _stream_init_loop(us_stream_s *stream) {
us_encoder_open(stream->enc, stream->cap);
return 0;
silent_error:
silent_capture_error:
if (!stream->exit_on_device_error) {
US_ONCE({ US_LOG_INFO("Waiting for the capture device ..."); });
}
silent_error:
goto offline_and_retry;
verbose_error:

View File

@@ -83,6 +83,7 @@ typedef struct {
uint desired_fps;
bool notify_parent;
bool slowdown;
uint open_on_demand;
uint error_delay;
bool exit_on_device_error;
uint exit_on_no_clients;