Compare commits

..

12 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
Maxim Devaev
0e64f1feda Bump version: v6.66 -> v6.67 2026-09-17 04:36:19 +03:00
Maxim Devaev
cfe83ae0bf governor scaling options 2026-09-17 03:39:16 +03:00
Maxim Devaev
864aac077e Bump version: v6.65 -> v6.66 2026-09-10 21:48:27 +03:00
Maxim Devaev
6952aff31d updated pkgbuild 2026-09-10 21:44:43 +03:00
Maxim Devaev
58e517b306 refactoring 2026-09-10 21:42:31 +03:00
sergey radionov
f6025267f6 janus, camera: aded non-IDR frames skip
after broken frame to avoid corrupted picture.
2026-09-10 21:41:44 +03:00
Maxim Devaev
3af6bfac0f typed list declaration 2026-08-30 00:16:47 +03:00
17 changed files with 143 additions and 42 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

@@ -36,7 +36,7 @@
#include "rtpc.h"
typedef struct {
typedef struct us_janus_client_sx {
janus_callbacks *gw;
janus_plugin_session *session;
@@ -66,7 +66,7 @@ typedef struct {
ldf last_key_req_ts;
US_LIST_DECLARE;
US_LIST_DECLARE(struct us_janus_client_sx);
} us_janus_client_s;

View File

@@ -43,7 +43,9 @@ enum {
};
enum {
_NALU_SPS = 7
_NALU_IDR = 5,
_NALU_SPS = 7,
_NALU_PPS = 8,
};
@@ -92,6 +94,7 @@ us_rtpc_unwrap_result_e us_rtpc_unwrap(
if (rtpc->fu && (rtpc->fu_rts != rts || fragment_type != _FR_FU_A)) {
rtpc->fu = false;
if (!rtpc->fu_is_bad) { // Not reported yet
rtpc->skip_until_key = true;
return US_RUR_DEPACKETIZATION_FAILED_RETRY;
}
}
@@ -156,6 +159,7 @@ us_rtpc_unwrap_result_e us_rtpc_unwrap(
if (!first_fragment) {
// US_LOG_INFO("Ignoring FU without a first fragment: seq=%u, rts=%u", seq, rts);
rtpc->fu_is_bad = true;
rtpc->skip_until_key = true;
return US_RUR_DEPACKETIZATION_FAILED;
}
rtpc->fu_is_bad = false;
@@ -181,6 +185,7 @@ us_rtpc_unwrap_result_e us_rtpc_unwrap(
} else {
// US_LOG_ERROR("Something went wrong with FU processing: seq=%u, rts=%u", seq, rts);
rtpc->fu_is_bad = true;
rtpc->skip_until_key = true;
return US_RUR_DEPACKETIZATION_FAILED;
}
@@ -282,6 +287,19 @@ static void _unwrapping_begin(us_rtpc_s *rtpc) {
static void _unwrapping_end(us_rtpc_s *rtpc) {
us_frame_s *const frame = rtpc->frame;
US_A(frame->used > 4); // NAL unit start code
const u8 nalu_type = (frame->data[4] & 0x1F);
if (rtpc->skip_until_key && nalu_type != _NALU_SPS && nalu_type != _NALU_PPS) {
if (nalu_type != _NALU_IDR) {
frame->used = 0;
return;
} else {
rtpc->skip_until_key = false;
}
}
frame->format = V4L2_PIX_FMT_H264;
frame->width = rtpc->frame_width;
frame->height = rtpc->frame_height;

View File

@@ -52,6 +52,8 @@ typedef struct { // *_rts == [R]TP [T]ime[S]tamp
u32 fu_rts;
bool fu_is_bad;
bool skip_until_key;
us_frame_s *frame;
uint frame_width;
uint frame_height;

View File

@@ -1,6 +1,6 @@
.\" Manpage for ustreamer-dump.
.\" Open an issue or pull request to https://github.com/pikvm/ustreamer to correct errors or typos
.TH USTREAMER-DUMP 1 "version 6.65" "January 2021"
.TH USTREAMER-DUMP 1 "version 6.67" "January 2021"
.SH NAME
ustreamer-dump \- Dump uStreamer's memory sink to file

View File

@@ -1,6 +1,6 @@
.\" Manpage for ustreamer.
.\" Open an issue or pull request to https://github.com/pikvm/ustreamer to correct errors or typos
.TH USTREAMER 1 "version 6.65" "November 2020"
.TH USTREAMER 1 "version 6.67" "November 2020"
.SH NAME
ustreamer \- stream MJPEG video from any V4L2 device to the network
@@ -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

@@ -1,16 +1,15 @@
# Contributor: Maxim Devaev <mdevaev@gmail.com>
# Author: Maxim Devaev <mdevaev@gmail.com>
# Maintainer: Maxim Devaev <mdevaev@gmail.com> [Software author]
pkgname=ustreamer
pkgver=6.65
pkgver=6.67
pkgrel=1
pkgdesc="Lightweight and fast MJPEG-HTTP streamer"
url="https://github.com/pikvm/ustreamer"
license=(GPL)
license=(GPL-3.0-or-later)
arch=(i686 x86_64 armv6h armv7h aarch64)
depends=(libjpeg-turbo libevent libbsd libgpiod systemd)
makedepends=(gcc make pkgconf systemd)
makedepends=(git)
source=(${pkgname}::"git+https://github.com/pikvm/ustreamer#commit=v${pkgver}")
md5sums=(SKIP)
@@ -19,7 +18,7 @@ _options="WITH_GPIO=1 WITH_SYSTEMD=1"
if [ -e /usr/bin/python3 ]; then
_options="$_options WITH_PYTHON=1"
depends+=("python>=3.14" "python<3.15")
makedepends+=(python-setuptools python-pip python-build python-wheel python-installer)
makedepends+=(python-setuptools python-build python-wheel python-installer)
fi
if [ -e /usr/include/janus/plugins/plugin.h ];then
depends+=(janus-gateway alsa-lib opus)

View File

@@ -9,7 +9,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=ustreamer
PKG_VERSION:=6.65
PKG_VERSION:=6.67
PKG_RELEASE:=1
PKG_MAINTAINER:=Maxim Devaev <mdevaev@gmail.com>

View File

@@ -34,7 +34,7 @@ def main() -> None:
flags = _find_flags()
setup(
name="ustreamer",
version="6.65",
version="6.67",
description="uStreamer tools",
author="Maxim Devaev",
author_email="mdevaev@gmail.com",

View File

@@ -26,7 +26,7 @@
#define US_VERSION_MAJOR 6
#define US_VERSION_MINOR 65
#define US_VERSION_MINOR 67
#define US_MAKE_VERSION2(_major, _minor) #_major "." #_minor
#define US_MAKE_VERSION1(_major, _minor) US_MAKE_VERSION2(_major, _minor)

View File

@@ -25,9 +25,9 @@
#include "tools.h"
#define US_LIST_DECLARE \
void *prev; \
void *next;
#define US_LIST_DECLARE(x_type) \
x_type *prev; \
x_type *next;
#define US_LIST_ITERATE(x_first, x_item, ...) { \
for (__typeof__(x_first) x_item = x_first; x_item;) { \

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

@@ -36,7 +36,7 @@
#include "../stream.h"
typedef struct {
typedef struct us_stream_client_sx {
struct us_server_sx *server;
struct evhttp_request *req;
@@ -54,15 +54,15 @@ typedef struct {
us_fpsi_s *fpsi;
US_LIST_DECLARE;
US_LIST_DECLARE(struct us_stream_client_sx);
} us_stream_client_s;
typedef struct {
typedef struct us_snapshot_client_sx {
struct us_server_sx *server;
struct evhttp_request *req;
ldf req_ts;
US_LIST_DECLARE;
US_LIST_DECLARE(struct us_snapshot_client_sx);
} us_snapshot_client_s;
typedef struct {

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,
@@ -156,6 +157,9 @@ enum _US_OPT_VALUES {
# endif
_O_NOTIFY_PARENT,
_O_CPU_SCALING_GOVERNOR_IDLE,
_O_CPU_SCALING_GOVERNOR_ACTIVE,
_O_LOG_LEVEL,
_O_PERF,
_O_VERBOSE,
@@ -187,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},
@@ -270,6 +275,9 @@ static const struct option _LONG_OPTS[] = {
# endif
{"notify-parent", no_argument, NULL, _O_NOTIFY_PARENT},
{"cpu-scaling-governor-idle", required_argument, NULL, _O_CPU_SCALING_GOVERNOR_IDLE},
{"cpu-scaling-governor-active", required_argument, NULL, _O_CPU_SCALING_GOVERNOR_ACTIVE},
{"log-level", required_argument, NULL, _O_LOG_LEVEL},
{"perf", no_argument, NULL, _O_PERF},
{"verbose", no_argument, NULL, _O_VERBOSE},
@@ -447,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);
@@ -550,6 +559,9 @@ int us_options_parse(
# endif
case _O_NOTIFY_PARENT: OPT_SET(stream->notify_parent, true);
case _O_CPU_SCALING_GOVERNOR_IDLE: OPT_SET(stream->governor_idle, optarg);
case _O_CPU_SCALING_GOVERNOR_ACTIVE: OPT_SET(stream->governor_active, optarg);
case _O_LOG_LEVEL: OPT_NUMBER("--log-level", us_g_log_level, US_LOG_LEVEL_INFO, US_LOG_LEVEL_DEBUG, 0);
case _O_PERF: OPT_SET(us_g_log_level, US_LOG_LEVEL_PERF);
case _O_VERBOSE: OPT_SET(us_g_log_level, US_LOG_LEVEL_VERBOSE);
@@ -740,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

@@ -94,7 +94,8 @@ static void _stream_drm_ensure_no_signal(us_stream_s *stream);
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);
static void _stream_check_suicide(us_stream_s *stream);
static void _stream_check_suicide(us_stream_s *stream, bool has_any_clients);
static void _stream_set_governor(us_stream_s *stream, bool active);
us_stream_s *us_stream_init(us_capture_s *cap, us_encoder_s *enc) {
@@ -204,8 +205,24 @@ void us_stream_loop(us_stream_s *stream) {
US_LOG_INFO("Capturing ...");
uint slowdown_count = 0;
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
@@ -233,17 +250,23 @@ void us_stream_loop(us_stream_s *stream) {
us_queue_put(releasers[hw->buf.index].q, hw, 0); // Plan to release
// Мы не обновляем здесь состояние синков, потому что это происходит внутри обслуживающих их потоков
_stream_check_suicide(stream);
if (stream->slowdown && !_stream_has_any_clients_cached(stream)) {
usleep(100 * 1000);
slowdown_count = (slowdown_count + 1) % 10;
if (slowdown_count > 0) {
continue;
const bool has_any_clients = _stream_has_any_clients_cached(stream);
if (has_any_clients_prev != has_any_clients) {
_stream_set_governor(stream, has_any_clients);
has_any_clients_prev = has_any_clients;
}
_stream_check_suicide(stream, has_any_clients);
if (stream->slowdown && !has_any_clients) {
usleep(100 * 1000);
}
}
close:
_stream_set_governor(stream, false);
atomic_store(&threads_stop, true);
# define DELETE_WORKER(x_ctx) if (x_ctx != NULL) { \
@@ -571,7 +594,18 @@ static int _stream_init_loop(us_stream_s *stream) {
UPDATE_SINK(stream->h264_sink);
# undef UPDATE_SINK
_stream_check_suicide(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
@@ -591,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"
@@ -599,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"
@@ -607,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"
@@ -615,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"
@@ -631,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:
@@ -767,7 +803,7 @@ done:
us_fpsi_update(run->http->h264_fpsi, meta.online, &meta);
}
static void _stream_check_suicide(us_stream_s *stream) {
static void _stream_check_suicide(us_stream_s *stream, bool has_any_clients) {
if (stream->exit_on_no_clients == 0) {
return;
}
@@ -775,7 +811,7 @@ static void _stream_check_suicide(us_stream_s *stream) {
atomic_ullong *last_req_ts = &stream->run->http->last_req_ts;
const ldf now_ts = us_get_now_monotonic();
if (_stream_has_any_clients_cached(stream)) {
if (has_any_clients) {
atomic_store(last_req_ts, now_ts);
} else if (atomic_load(last_req_ts) + stream->exit_on_no_clients < now_ts) {
US_LOG_INFO("No requests or HTTP/sink clients found in last %u seconds, exiting ...",
@@ -784,3 +820,26 @@ static void _stream_check_suicide(us_stream_s *stream) {
us_process_suicide();
}
}
static void _stream_set_governor(us_stream_s *stream, bool active) {
if (!us_str_is_ok(stream->governor_idle) || !us_str_is_ok(stream->governor_active)) {
return;
}
const char *const path = "/sys/devices/system/cpu/cpufreq/policy0/scaling_governor";
const int fd = open(path, O_RDWR);
if (fd < 0) {
US_LOG_PERROR("Can't open CPU governor file");
return;
}
const char *const gov = (active ? stream->governor_active : stream->governor_idle);
const sz len = strlen(gov);
if (write(fd, gov, len) != len) {
US_LOG_PERROR("Can't set CPU governor to %s", gov);
} else {
US_LOG_INFO("CPU governor changed to %s", gov);
}
close(fd);
}

View File

@@ -83,10 +83,14 @@ 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;
char *governor_idle;
char *governor_active;
us_memsink_s *jpeg_sink;
us_memsink_s *raw_sink;

View File

@@ -47,7 +47,7 @@ typedef struct us_worker_sx {
struct us_workers_pool_sx *pool;
US_LIST_DECLARE;
US_LIST_DECLARE(struct us_worker_sx);
} us_worker_s;
typedef void *(*us_workers_pool_job_init_f)(void *arg);