Compare commits

...

16 Commits
v6.37 ... v6.43

Author SHA1 Message Date
Maxim Devaev
e2b01e4d79 Bump version: 6.42 → 6.43 2026-01-03 19:43:43 +02:00
Maxim Devaev
903bc45bee lint fixes 2026-01-03 19:21:10 +02:00
Maxim Devaev
b2b1989c5b reduced preallocated us_frame_s size 2026-01-03 18:54:56 +02:00
Maxim Devaev
36b539c275 Bump version: 6.41 → 6.42 2025-11-11 00:00:35 +02:00
Maxim Devaev
38c6917644 janus: pkg-config 2025-11-10 23:58:42 +02:00
Maxim Devaev
05a5d3fed4 Bump version: 6.40 → 6.41 2025-10-23 16:28:07 +03:00
Maxim Devaev
0e4bf31325 janus: non-tc358743 devices for acap suppurted
An alternative implementation of pikvm/ustreamer#304.
Thanks for the idea.
2025-10-23 00:50:19 +03:00
Maxim Devaev
9a5cce3b92 janus: deprecated aplay/check option 2025-10-22 21:35:56 +03:00
Maxim Devaev
c4ac67acba janus: plug audio devices dynamically 2025-10-22 19:35:35 +03:00
Maxim Devaev
472673ea90 Bump version: 6.39 → 6.40 2025-07-28 21:32:04 +03:00
Maxim Devaev
f7ebe31c71 refactoring 2025-07-28 21:29:27 +03:00
Maxim Devaev
3a831817f4 pikvm/pikvm#1558: Discard JPEGs with invalid headers 2025-07-28 21:26:08 +03:00
Maxim Devaev
913cdac7a6 Bump version: 6.38 → 6.39 2025-07-03 04:19:51 +03:00
Maxim Devaev
777697dc1e improved logging on --exit-on-device-error 2025-07-03 04:17:48 +03:00
Maxim Devaev
5f437b9a35 Bump version: 6.37 → 6.38 2025-07-03 03:51:05 +03:00
Maxim Devaev
b089f896da pikvm/pikvm#312: --exit-on-device-error 2025-07-03 03:49:02 +03:00
22 changed files with 163 additions and 58 deletions

View File

@@ -1,7 +1,7 @@
[bumpversion]
commit = True
tag = True
current_version = 6.37
current_version = 6.43
parse = (?P<major>\d+)\.(?P<minor>\d+)
serialize =
{major}.{minor}

View File

@@ -10,8 +10,8 @@ LDFLAGS ?=
# =====
_PLUGIN = libjanus_ustreamer.so
_CFLAGS = -fPIC -MD -c -std=c17 -Wall -Wextra -D_GNU_SOURCE $(shell $(PKG_CONFIG) --cflags glib-2.0) $(CFLAGS)
_LDFLAGS = -shared -lm -pthread -lrt -ljansson -lopus -lasound -lspeexdsp $(shell $(PKG_CONFIG) --libs glib-2.0) $(LDFLAGS)
_CFLAGS = -fPIC -MD -c -std=c17 -Wall -Wextra -D_GNU_SOURCE $(shell $(PKG_CONFIG) --cflags janus-gateway) $(CFLAGS)
_LDFLAGS = -shared -lm -pthread -lrt -ljansson -lopus -lasound -lspeexdsp $(shell $(PKG_CONFIG) --libs janus-gateway) $(LDFLAGS)
_SRCS = $(shell ls src/uslibs/*.c src/*.c)

View File

@@ -47,19 +47,6 @@ static void *_pcm_thread(void *v_acap);
static void *_encoder_thread(void *v_acap);
bool us_acap_probe(const char *name) {
snd_pcm_t *dev;
int err;
US_JLOG_INFO("acap", "Probing PCM capture ...");
if ((err = snd_pcm_open(&dev, name, SND_PCM_STREAM_CAPTURE, 0)) < 0) {
US_JLOG_PERROR_ALSA(err, "acap", "Can't probe PCM capture");
return false;
}
snd_pcm_close(dev);
US_JLOG_INFO("acap", "PCM capture is available");
return true;
}
us_acap_s *us_acap_init(const char *name, uint pcm_hz) {
us_acap_s *acap;
US_CALLOC(acap, 1);

View File

@@ -53,8 +53,6 @@ typedef struct {
} us_acap_s;
bool us_acap_probe(const char *name);
us_acap_s *us_acap_init(const char *name, uint pcm_hz);
void us_acap_destroy(us_acap_s *acap);

View File

@@ -23,10 +23,78 @@
#include "au.h"
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <sys/stat.h>
#include "uslibs/tools.h"
bool us_au_probe(const char *name) {
// This function is very limited. It takes something like:
// hw:0,0 or hw:tc358743,0 or plughw:UAC2Gadget,0
// parses card name (0, tc358743, UAC2Gadget) and checks
// the existence of it in /proc/asound/.
// It's enough for our case.
if (name == NULL) {
return false;
}
if (strchr(name, '/') || strchr(name, '.')) {
return false;
}
const char *begin = strchr(name, ':');
if (begin == NULL) {
return false;
}
begin += 1;
if (*begin == '\0') {
return false;
}
const char *end = strchr(begin, ',');
if (end == NULL) {
return false;
}
if (end - begin < 1) {
return false;
}
char *card = us_strdup(begin);
card[end - begin] = '\0';
bool numeric = true;
for (uz index = 0; card[index] != '\0'; ++index) {
if (!isdigit(card[index])) {
numeric = false;
break;
}
}
char *path;
if (numeric) {
US_ASPRINTF(path, "/proc/asound/card%s", card);
} else {
US_ASPRINTF(path, "/proc/asound/%s", card);
}
bool ok = false;
struct stat st;
if (lstat(path, &st) == 0) {
if (numeric && S_ISDIR(st.st_mode)) {
ok = true;
} else if (!numeric && S_ISLNK(st.st_mode)) {
ok = true;
}
}
free(path);
free(card);
return ok;
}
us_au_pcm_s *us_au_pcm_init(void) {
us_au_pcm_s *pcm;
US_CALLOC(pcm, 1);

View File

@@ -51,6 +51,7 @@ typedef struct {
u64 pts;
} us_au_encoded_s;
bool us_au_probe(const char *name);
us_au_pcm_s *us_au_pcm_init(void);
void us_au_pcm_destroy(us_au_pcm_s *pcm);

View File

@@ -24,11 +24,11 @@
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <janus/config.h>
#include <janus/plugins/plugin.h>
#include "uslibs/types.h"
#include "uslibs/tools.h"
#include "const.h"
@@ -36,6 +36,7 @@
static char *_get_value(janus_config *jcfg, const char *section, const char *option);
static uint _get_uint(janus_config *jcfg, const char *section, const char *option, uint def);
// static bool _get_bool(janus_config *jcfg, const char *section, const char *option, bool def);
@@ -61,20 +62,13 @@ us_config_s *us_config_init(const char *config_dir_path) {
goto error;
}
if ((config->acap_dev_name = _get_value(jcfg, "acap", "device")) != NULL) {
if ((config->tc358743_dev_path = _get_value(jcfg, "acap", "tc358743")) == NULL) {
US_JLOG_INFO("config", "Missing config value: acap.tc358743");
config->acap_hz = _get_uint(jcfg, "acap", "sampling_rate", 0);
config->tc358743_dev_path = _get_value(jcfg, "acap", "tc358743");
if (config->acap_hz == 0 && config->tc358743_dev_path == NULL) {
US_JLOG_ERROR("config", "Either acap.sampling_rate or acap.tc358743 required");
goto error;
}
if ((config->aplay_dev_name = _get_value(jcfg, "aplay", "device")) != NULL) {
char *path = _get_value(jcfg, "aplay", "check");
if (path != NULL) {
if (access(path, F_OK) != 0) {
US_JLOG_INFO("config", "No check file found, aplay will be disabled");
US_DELETE(config->aplay_dev_name, free);
}
US_DELETE(path, free);
}
}
config->aplay_dev_name = _get_value(jcfg, "aplay", "device");
}
goto ok;
@@ -105,6 +99,20 @@ static char *_get_value(janus_config *jcfg, const char *section, const char *opt
return us_strdup(option_obj->value);
}
static uint _get_uint(janus_config *jcfg, const char *section, const char *option, uint def) {
char *const tmp = _get_value(jcfg, section, option);
uint value = def;
if (tmp != NULL) {
errno = 0;
value = (uint)strtoul(tmp, NULL, 10);
if (errno != 0) {
value = def;
}
free(tmp);
}
return value;
}
/*static bool _get_bool(janus_config *jcfg, const char *section, const char *option, bool def) {
char *const tmp = _get_value(jcfg, section, option);
bool value = def;

View File

@@ -23,10 +23,14 @@
#pragma once
#include "uslibs/types.h"
typedef struct {
char *video_sink_name;
char *acap_dev_name;
uint acap_hz;
char *tc358743_dev_path;
char *aplay_dev_name;

View File

@@ -69,7 +69,7 @@ static us_janus_client_s *_g_clients = NULL;
static janus_callbacks *_g_gw = NULL;
static us_ring_s *_g_video_ring = NULL;
static us_rtpv_s *_g_rtpv = NULL;
static us_rtpa_s *_g_rtpa = NULL; // Also indicates "audio capture is available"
static us_rtpa_s *_g_rtpa = NULL;
static pthread_t _g_video_rtp_tid;
static atomic_bool _g_video_rtp_tid_created = false;
@@ -214,7 +214,15 @@ static void *_video_sink_thread(void *arg) {
return NULL;
}
static int _check_tc358743_acap(uint *hz) {
static int _get_acap_hz(uint *hz) {
if (_g_config->acap_hz != 0) {
*hz = _g_config->acap_hz;
return 0;
}
if (_g_config->tc358743_dev_path == NULL) {
US_JLOG_ERROR("acap", "No configured sampling rate");
return -1;
}
int fd;
if ((fd = open(_g_config->tc358743_dev_path, O_RDWR)) < 0) {
US_JLOG_PERROR("acap", "Can't open TC358743 V4L2 device");
@@ -236,7 +244,6 @@ static void *_acap_thread(void *arg) {
atomic_store(&_g_acap_tid_created, true);
assert(_g_config->acap_dev_name != NULL);
assert(_g_config->tc358743_dev_path != NULL);
assert(_g_rtpa != NULL);
int once = 0;
@@ -250,7 +257,11 @@ static void *_acap_thread(void *arg) {
uint hz = 0;
us_acap_s *acap = NULL;
if (_check_tc358743_acap(&hz) < 0) {
if (!us_au_probe(_g_config->acap_dev_name)) {
US_ONCE({ US_JLOG_ERROR("acap", "No PCM capture device"); });
goto close_acap;
}
if (_get_acap_hz(&hz) < 0) {
goto close_acap;
}
if (hz == 0) {
@@ -265,7 +276,7 @@ static void *_acap_thread(void *arg) {
once = 0;
while (!_STOP && _HAS_WATCHERS && _HAS_LISTENERS) {
if (_check_tc358743_acap(&hz) < 0 || acap->pcm_hz != hz) {
if (_get_acap_hz(&hz) < 0 || acap->pcm_hz != hz) {
goto close_acap;
}
uz size = US_RTP_DATAGRAM_SIZE - US_RTP_HEADER_SIZE;
@@ -339,6 +350,11 @@ static void *_aplay_thread(void *arg) {
}
if (dev == NULL) {
if (!us_au_probe(_g_config->aplay_dev_name)) {
US_ONCE({ US_JLOG_ERROR("aplay", "No PCM playback device"); });
goto close_aplay;
}
int err = snd_pcm_open(&dev, _g_config->aplay_dev_name, SND_PCM_STREAM_PLAYBACK, 0);
if (err < 0) {
US_ONCE({ US_JLOG_PERROR_ALSA(err, "aplay", "Can't open PCM playback"); });
@@ -424,7 +440,7 @@ static int _plugin_init(janus_callbacks *gw, const char *config_dir_path) {
US_RING_INIT_WITH_ITEMS(_g_video_ring, 64, us_frame_init);
_g_rtpv = us_rtpv_init(_relay_rtp_clients);
if (_g_config->acap_dev_name != NULL && us_acap_probe(_g_config->acap_dev_name)) {
if (_g_config->acap_dev_name != NULL) {
_g_rtpa = us_rtpa_init(_relay_rtp_clients);
US_THREAD_CREATE(_g_acap_tid, _acap_thread, NULL);
if (_g_config->aplay_dev_name != NULL) {
@@ -602,13 +618,13 @@ static struct janus_plugin_result *_plugin_handle_message(
{
json_t *const obj = json_object_get(params, "audio");
if (obj != NULL && json_is_boolean(obj)) {
with_acap = (_g_rtpa != NULL && json_boolean_value(obj));
with_acap = (us_au_probe(_g_config->acap_dev_name) && json_boolean_value(obj));
}
}
{
json_t *const obj = json_object_get(params, "mic");
if (obj != NULL && json_is_boolean(obj)) {
with_aplay = (_g_config->aplay_dev_name != NULL && with_acap && json_boolean_value(obj));
with_aplay = (us_au_probe(_g_config->aplay_dev_name) && json_boolean_value(obj));
}
}
{
@@ -673,10 +689,11 @@ static struct janus_plugin_result *_plugin_handle_message(
} else if (!strcmp(request_str, "features")) {
const char *const ice_url = getenv("JANUS_USTREAMER_WEB_ICE_URL");
const bool acap_avail = us_au_probe(_g_config->acap_dev_name);
json_t *const features = json_pack(
"{s:b, s:b, s:{s:s?}}",
"audio", (_g_rtpa != NULL),
"mic", (_g_rtpa != NULL && _g_config->aplay_dev_name != NULL),
"audio", acap_avail,
"mic", (acap_avail && us_au_probe(_g_config->aplay_dev_name)),
"ice", "url", (ice_url != NULL ? ice_url : default_ice_url)
);
PUSH_STATUS("features", features, NULL);

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.37" "January 2021"
.TH USTREAMER-DUMP 1 "version 6.43" "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.37" "November 2020"
.TH USTREAMER 1 "version 6.43" "November 2020"
.SH NAME
ustreamer \- stream MJPEG video from any V4L2 device to the network
@@ -276,6 +276,9 @@ Timeout for lock. Default: 1.
.BR \-\-exit\-on\-parent\-death
Exit the program if the parent process is dead. Required \fBWITH_PDEATHSIG\fR feature. Default: disabled.
.TP
.BR \-\-exit\-on\-device\-error
Exit on any device error instead of polling until success. Default: disabled.
.TP
.BR \-\-exit\-on\-no\-clients \fIsec
Exit the program if there have been no stream or sink clients or any HTTP requests in the last N seconds. Default: 0 (disabled).
.TP

View File

@@ -3,7 +3,7 @@
pkgname=ustreamer
pkgver=6.37
pkgver=6.43
pkgrel=1
pkgdesc="Lightweight and fast MJPEG-HTTP streamer"
url="https://github.com/pikvm/ustreamer"

View File

@@ -6,7 +6,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=ustreamer
PKG_VERSION:=6.37
PKG_VERSION:=6.43
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.37",
version="6.43",
description="uStreamer tools",
author="Maxim Devaev",
author_email="mdevaev@gmail.com",

View File

@@ -569,19 +569,28 @@ bool _capture_is_buffer_valid(const us_capture_s *cap, const struct v4l2_buffer
if (us_is_jpeg(cap->run->format)) {
if (buf->bytesused < 125) {
// https://stackoverflow.com/questions/2253404/what-is-the-smallest-valid-jpeg-file-size-in-bytes
_LOG_DEBUG("Discarding invalid frame, too small to be a valid JPEG: bytesused=%u", buf->bytesused);
_LOG_DEBUG("Discarding invalid frame, too small to be a valid JPEG: bytesused=%u",
buf->bytesused);
return false;
}
const u8 *const end_ptr = data + buf->bytesused;
const u8 *const eoi_ptr = end_ptr - 2;
const u16 eoi_marker = (((u16)(eoi_ptr[0]) << 8) | eoi_ptr[1]);
if (eoi_marker != 0xFFD9 && eoi_marker != 0xD900 && eoi_marker != 0x0000) {
const u16 begin_marker = (((u16)(data[0]) << 8) | data[1]);
if (begin_marker != 0xFFD8) {
_LOG_DEBUG("Discarding JPEG frame with invalid header: begin_marker=0x%04x, bytesused=%u",
begin_marker, buf->bytesused);
return false;
}
const u8 *const end_ptr = data + buf->bytesused - 2;
const u16 end_marker = (((u16)(end_ptr[0]) << 8) | end_ptr[1]);
if (end_marker != 0xFFD9 && end_marker != 0xD900 && end_marker != 0x0000) {
if (!cap->allow_truncated_frames) {
_LOG_DEBUG("Discarding truncated JPEG frame: eoi_marker=0x%04x, bytesused=%u", eoi_marker, buf->bytesused);
_LOG_DEBUG("Discarding truncated JPEG frame: end_marker=0x%04x, bytesused=%u",
end_marker, buf->bytesused);
return false;
}
_LOG_DEBUG("Got truncated JPEG frame: eoi_marker=0x%04x, bytesused=%u", eoi_marker, buf->bytesused);
_LOG_DEBUG("Got truncated JPEG frame: end_marker=0x%04x, bytesused=%u",
end_marker, buf->bytesused);
}
}

View File

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

View File

@@ -36,7 +36,7 @@
us_frame_s *us_frame_init(void) {
us_frame_s *frame;
US_CALLOC(frame, 1);
us_frame_realloc_data(frame, 512 * 1024);
us_frame_realloc_data(frame, 32 * 1024);
frame->dma_fd = -1;
return frame;
}

View File

@@ -131,7 +131,7 @@ void us_encoder_open(us_encoder_s *enc, us_capture_s *cap) {
} else {
US_LOG_INFO("Switching to CPU encoder: the input format is not (M)JPEG ...");
type = US_ENCODER_TYPE_CPU;
quality = cap->jpeg_quality;
quality = cap->jpeg_quality; // cppcheck-suppress redundantAssignment
}
} else if (type == US_ENCODER_TYPE_M2M_VIDEO || type == US_ENCODER_TYPE_M2M_IMAGE) {

View File

@@ -117,6 +117,7 @@ enum _US_OPT_VALUES {
# ifdef WITH_PDEATHSIG
_O_EXIT_ON_PARENT_DEATH,
# endif
_O_EXIT_ON_DEVICE_ERROR,
_O_EXIT_ON_NO_CLIENTS,
# ifdef WITH_SETPROCTITLE
_O_PROCESS_NAME_PREFIX,
@@ -227,6 +228,7 @@ static const struct option _LONG_OPTS[] = {
# ifdef WITH_PDEATHSIG
{"exit-on-parent-death", no_argument, NULL, _O_EXIT_ON_PARENT_DEATH},
# endif
{"exit-on-device-error", no_argument, NULL, _O_EXIT_ON_DEVICE_ERROR},
{"exit-on-no-clients", required_argument, NULL, _O_EXIT_ON_NO_CLIENTS},
# ifdef WITH_SETPROCTITLE
{"process-name-prefix", required_argument, NULL, _O_PROCESS_NAME_PREFIX},
@@ -490,6 +492,7 @@ int options_parse(us_options_s *options, us_capture_s *cap, us_encoder_s *enc, u
};
break;
# endif
case _O_EXIT_ON_DEVICE_ERROR: OPT_SET(stream->exit_on_device_error, true);
case _O_EXIT_ON_NO_CLIENTS: OPT_NUMBER("--exit-on-no-clients", stream->exit_on_no_clients, 0, 86400, 0);
# ifdef WITH_SETPROCTITLE
case _O_PROCESS_NAME_PREFIX: OPT_SET(process_name_prefix, optarg);

View File

@@ -604,7 +604,9 @@ static int _stream_init_loop(us_stream_s *stream) {
return 0;
silent_error:
US_ONCE({ US_LOG_INFO("Waiting for the capture device ..."); });
if (!stream->exit_on_device_error) {
US_ONCE({ US_LOG_INFO("Waiting for the capture device ..."); });
}
goto offline_and_retry;
verbose_error:
@@ -612,6 +614,10 @@ static int _stream_init_loop(us_stream_s *stream) {
goto offline_and_retry;
offline_and_retry:
if (stream->exit_on_device_error) {
US_LOG_INFO("Device error, exiting ...");
us_process_suicide();
}
for (uint count = 0; count < stream->error_delay * 10; ++count) {
if (atomic_load(&run->stop)) {
break;

View File

@@ -80,6 +80,7 @@ typedef struct {
bool notify_parent;
bool slowdown;
uint error_delay;
bool exit_on_device_error;
uint exit_on_no_clients;
us_memsink_s *jpeg_sink;

View File

@@ -242,7 +242,7 @@ static void _main_loop(void) {
us_drm_destroy(drm);
}
static void *_follower_thread(void *v_unix_follow) {
static void *_follower_thread(void *v_unix_follow) { // cppcheck-suppress constParameterCallback
US_THREAD_SETTLE("follower");
const char *path = v_unix_follow;
assert(path != NULL);