--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>
This commit is contained in:
Stephen Green
2026-09-27 11:29:51 -04:00
parent 0e64f1feda
commit 7cde878f02
4 changed files with 71 additions and 17 deletions

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
Don't open the capture device until at least one stream or sink client is connected, and close it again 3 seconds after 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

@@ -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", no_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_SET(stream->open_on_demand, true);
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 ──────────────────── Don't open the capture device until at least one stream");
SAY(" or sink client is connected, and close it again %u seconds", US_ON_DEMAND_CLOSE_DELAY);
SAY(" after the last one disconnects. Clients see a blank");
SAY(" 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

@@ -91,6 +91,7 @@ static void _stream_update_captured_fpsi(us_stream_s *stream, const us_frame_s *
#ifdef WITH_V4P
static void _stream_drm_ensure_no_signal(us_stream_s *stream);
#endif
static void _stream_publish_blank(us_stream_s *stream, const char *reason);
static void _stream_expose_jpeg(us_stream_s *stream, const us_frame_s *frame);
static void _stream_expose_raw(us_stream_s *stream, const us_frame_s *frame);
static void _stream_encode_expose_h264(us_stream_s *stream, const us_frame_s *frame, bool force_key);
@@ -206,7 +207,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) {
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 + US_ON_DEMAND_CLOSE_DELAY < now_ts) {
US_LOG_INFO("No stream or sink clients found in last %u seconds, closing the device ...",
US_ON_DEMAND_CLOSE_DELAY);
goto close;
}
}
}
us_capture_hwbuf_s *hw;
switch (us_capture_hwbuf_grab(cap, &hw)) {
case 0 ... INT_MAX: break; // Grabbed buffer number
@@ -580,6 +597,23 @@ static int _stream_init_loop(us_stream_s *stream) {
_stream_check_suicide(stream, _stream_has_any_clients_cached(stream));
if (stream->open_on_demand && !_stream_has_any_clients_cached(stream)) {
US_ONCE({ US_LOG_INFO("Waiting for the stream or sink clients to open the device ..."); });
_stream_publish_blank(stream, (
"< CAMERA IS IDLE >\n \n"
" The device will \n \n"
" be opened on the \n \n"
" first client "
));
for (uint i = 0; i < 10; ++i) {
if (atomic_load(&run->stop) || _stream_has_any_clients_cached(stream)) {
break;
}
usleep(100 * 1000);
}
continue;
}
stream->cap->dma_export = (
stream->enc->type == US_ENCODER_TYPE_M2M_VIDEO
|| stream->enc->type == US_ENCODER_TYPE_M2M_IMAGE
@@ -659,22 +693,7 @@ static int _stream_init_loop(us_stream_s *stream) {
}
if (i % 10 == 0) {
// Каждую секунду повторяем blank
uint width = stream->cap->run->width;
uint height = stream->cap->run->height;
if (width == 0 || height == 0) {
width = stream->cap->width;
height = stream->cap->height;
}
us_blank_draw(run->blank, blank_reason, width, height);
_stream_update_captured_fpsi(stream, run->blank->raw, false);
_stream_expose_jpeg(stream, run->blank->jpeg);
_stream_expose_raw(stream, run->blank->raw);
_stream_encode_expose_h264(stream, run->blank->raw, true);
# ifdef WITH_V4P
_stream_drm_ensure_no_signal(stream);
# endif
_stream_publish_blank(stream, blank_reason);
}
usleep(100 * 1000);
}
@@ -720,6 +739,27 @@ close:
}
#endif
static void _stream_publish_blank(us_stream_s *stream, const char *reason) {
us_stream_runtime_s *const run = stream->run;
uint width = stream->cap->run->width;
uint height = stream->cap->run->height;
if (width == 0 || height == 0) {
width = stream->cap->width;
height = stream->cap->height;
}
us_blank_draw(run->blank, reason, width, height);
_stream_update_captured_fpsi(stream, run->blank->raw, false);
_stream_expose_jpeg(stream, run->blank->jpeg);
_stream_expose_raw(stream, run->blank->raw);
_stream_encode_expose_h264(stream, run->blank->raw, true);
# ifdef WITH_V4P
_stream_drm_ensure_no_signal(stream);
# endif
}
static void _stream_expose_jpeg(us_stream_s *stream, const us_frame_s *frame) {
us_stream_runtime_s *const run = stream->run;

View File

@@ -44,6 +44,9 @@
#include "m2m.h"
#define US_ON_DEMAND_CLOSE_DELAY 3u
typedef struct {
# ifdef WITH_V4P
atomic_bool drm_live;
@@ -83,6 +86,7 @@ typedef struct {
uint desired_fps;
bool notify_parent;
bool slowdown;
bool open_on_demand;
uint error_delay;
bool exit_on_device_error;
uint exit_on_no_clients;