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 # These are supported funding model platforms
patreon: pikvm 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 .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. Slowdown capturing to 1 FPS or less when no stream or sink clients are connected. Useful to reduce CPU consumption. Default: disabled.
.TP .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 .BR \-\-device\-timeout\ \fIsec
Timeout for device querying. Default: 1. Timeout for device querying. Default: 1.
.TP .TP

View File

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

View File

@@ -89,7 +89,8 @@ enum _US_OPT_VALUES {
// Longs only // Longs only
_O_DEVICE_TIMEOUT = 10000, _O_OPEN_ON_DEMAND = 10000,
_O_DEVICE_TIMEOUT,
_O_DEVICE_ERROR_DELAY, _O_DEVICE_ERROR_DELAY,
_O_FORMAT_SWAP_RGB, _O_FORMAT_SWAP_RGB,
_O_M2M_DEVICE, _O_M2M_DEVICE,
@@ -190,6 +191,7 @@ static const struct option _LONG_OPTS[] = {
{"blank", required_argument, NULL, _O_BLANK}, {"blank", required_argument, NULL, _O_BLANK},
{"last-as-blank", required_argument, NULL, _O_LAST_AS_BLANK}, {"last-as-blank", required_argument, NULL, _O_LAST_AS_BLANK},
{"slowdown", no_argument, NULL, _O_SLOWDOWN}, {"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-timeout", required_argument, NULL, _O_DEVICE_TIMEOUT},
{"device-error-delay", required_argument, NULL, _O_DEVICE_ERROR_DELAY}, {"device-error-delay", required_argument, NULL, _O_DEVICE_ERROR_DELAY},
{"m2m-device", required_argument, NULL, _O_M2M_DEVICE}, {"m2m-device", required_argument, NULL, _O_M2M_DEVICE},
@@ -453,6 +455,7 @@ int us_options_parse(
case _O_BLANK: break; // Deprecated case _O_BLANK: break; // Deprecated
case _O_LAST_AS_BLANK: break; // Deprecated case _O_LAST_AS_BLANK: break; // Deprecated
case _O_SLOWDOWN: OPT_SET(stream->slowdown, true); 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_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_DEVICE_ERROR_DELAY: OPT_NUMBER("--device-error-delay", stream->error_delay, 1, 60, 0);
case _O_M2M_DEVICE: OPT_SET(enc->m2m_path, optarg); 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(" -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(" -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(" 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-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(" --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); 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 ..."); US_LOG_INFO("Capturing ...");
int has_any_clients_prev = -1; int has_any_clients_prev = -1;
ldf no_clients_since_ts = 0;
while (!atomic_load(&run->stop) && !atomic_load(&threads_stop)) { 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; us_capture_hwbuf_s *hw;
switch (us_capture_hwbuf_grab(cap, &hw)) { switch (us_capture_hwbuf_grab(cap, &hw)) {
case 0 ... INT_MAX: break; // Grabbed buffer number 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); UPDATE_SINK(stream->h264_sink);
# undef UPDATE_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->cap->dma_export = (
stream->enc->type == US_ENCODER_TYPE_M2M_VIDEO 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" " - Bad config \n \n"
" - Malfunction " " - Malfunction "
); );
goto silent_error; goto silent_capture_error;
case US_ERROR_NO_CABLE: case US_ERROR_NO_CABLE:
blank_reason = ( blank_reason = (
"< NO VIDEO SOURCE >\n \n" "< NO VIDEO SOURCE >\n \n"
@@ -606,7 +633,7 @@ static int _stream_init_loop(us_stream_s *stream) {
" - Source is off \n \n" " - Source is off \n \n"
" - Cable problems " " - Cable problems "
); );
goto silent_error; goto silent_capture_error;
case US_ERROR_NO_SIGNAL: case US_ERROR_NO_SIGNAL:
blank_reason = ( blank_reason = (
"< NO SIGNAL DETECTED >\n \n" "< NO SIGNAL DETECTED >\n \n"
@@ -614,7 +641,7 @@ static int _stream_init_loop(us_stream_s *stream) {
" - Video suspended \n \n" " - Video suspended \n \n"
" - Cable problems " " - Cable problems "
); );
goto silent_error; goto silent_capture_error;
case US_ERROR_NO_SYNC: case US_ERROR_NO_SYNC:
blank_reason = ( blank_reason = (
"< NO SYNC WITH SIGNAL >\n \n" "< NO SYNC WITH SIGNAL >\n \n"
@@ -622,7 +649,7 @@ static int _stream_init_loop(us_stream_s *stream) {
" - Source is crazy \n \n" " - Source is crazy \n \n"
" - Cable problems " " - Cable problems "
); );
goto silent_error; goto silent_capture_error;
case US_ERROR_NO_LANES: case US_ERROR_NO_LANES:
blank_reason = ( blank_reason = (
"< UNSUPPORTED SIGNAL TIMINGS >\n \n" "< 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); us_encoder_open(stream->enc, stream->cap);
return 0; return 0;
silent_error: silent_capture_error:
if (!stream->exit_on_device_error) { if (!stream->exit_on_device_error) {
US_ONCE({ US_LOG_INFO("Waiting for the capture device ..."); }); US_ONCE({ US_LOG_INFO("Waiting for the capture device ..."); });
} }
silent_error:
goto offline_and_retry; goto offline_and_retry;
verbose_error: verbose_error:

View File

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