From 7cde878f02d479647e987e8aa3a2f05faffb04e9 Mon Sep 17 00:00:00 2001 From: Stephen Green Date: Sun, 27 Sep 2026 11:29:51 -0400 Subject: [PATCH] --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 --- man/ustreamer.1 | 3 ++ src/ustreamer/options.c | 9 +++++- src/ustreamer/stream.c | 72 ++++++++++++++++++++++++++++++++--------- src/ustreamer/stream.h | 4 +++ 4 files changed, 71 insertions(+), 17 deletions(-) diff --git a/man/ustreamer.1 b/man/ustreamer.1 index e08bd91..e270e04 100644 --- a/man/ustreamer.1 +++ b/man/ustreamer.1 @@ -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 diff --git a/src/ustreamer/options.c b/src/ustreamer/options.c index 8a177ed..88c7c2c 100644 --- a/src/ustreamer/options.c +++ b/src/ustreamer/options.c @@ -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 ──────────── 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 ────────────── Timeout for device querying. Default: %u.\n", cap->timeout); SAY(" --device-error-delay ────────── Delay before trying to connect to the device again"); SAY(" after an error (timeout for example). Default: %u.\n", stream->error_delay); diff --git a/src/ustreamer/stream.c b/src/ustreamer/stream.c index 43a6131..3c7fdbf 100644 --- a/src/ustreamer/stream.c +++ b/src/ustreamer/stream.c @@ -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; diff --git a/src/ustreamer/stream.h b/src/ustreamer/stream.h index 9020cf9..7112a23 100644 --- a/src/ustreamer/stream.h +++ b/src/ustreamer/stream.h @@ -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;