mirror of
https://github.com/pikvm/ustreamer.git
synced 2026-02-19 16:26:30 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de2cfa36e1 | ||
|
|
6c1a8f75a1 | ||
|
|
26ee5143ee | ||
|
|
e2890e5851 | ||
|
|
e2b01e4d79 | ||
|
|
903bc45bee | ||
|
|
b2b1989c5b | ||
|
|
36b539c275 | ||
|
|
38c6917644 | ||
|
|
05a5d3fed4 | ||
|
|
0e4bf31325 | ||
|
|
9a5cce3b92 | ||
|
|
c4ac67acba | ||
|
|
472673ea90 | ||
|
|
f7ebe31c71 | ||
|
|
3a831817f4 | ||
|
|
913cdac7a6 | ||
|
|
777697dc1e | ||
|
|
5f437b9a35 | ||
|
|
b089f896da | ||
|
|
0e521ad0c6 | ||
|
|
620a0ec847 | ||
|
|
7a1d4816ed | ||
|
|
aec8431024 | ||
|
|
5b18e29555 | ||
|
|
2717248581 | ||
|
|
afd305e87d | ||
|
|
e3d8132237 | ||
|
|
1f32e875c3 |
@@ -1,7 +1,7 @@
|
||||
[bumpversion]
|
||||
commit = True
|
||||
tag = True
|
||||
current_version = 6.35
|
||||
current_version = 6.45
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)
|
||||
serialize =
|
||||
{major}.{minor}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|----------|---------------|-------------------|
|
||||
| Multithreaded JPEG encoding | ✔ | ✘ |
|
||||
| Hardware image encoding<br>on Raspberry Pi | ✔ | ✘ |
|
||||
| Behavior when the device<br>is disconnected while streaming | ✔ Shows a black screen<br>with ```NO SIGNAL``` on it<br>until reconnected | ✘ Stops the streaming <sup>1</sup> |
|
||||
| Behavior when the device<br>is disconnected while streaming | ✔ Shows a black screen<br>with ```NO LIVE VIDEO``` on it<br>until reconnected | ✘ Stops the streaming <sup>1</sup> |
|
||||
| [DV-timings](https://linuxtv.org/downloads/v4l-dvb-apis-new/userspace-api/v4l/dv-timings.html) support -<br>the ability to change resolution<br>on the fly by source signal | ✔ | ☹ Partially yes <sup>1</sup> |
|
||||
| Option to skip frames when streaming<br>static images by HTTP to save the traffic | ✔ <sup>2</sup> | ✘ |
|
||||
| Streaming via UNIX domain socket | ✔ | ✘ |
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -54,6 +54,8 @@ char *us_rtpa_make_sdp(us_rtpa_s *rtpa, bool mic) {
|
||||
"a=rtcp-fb:%u nack" RN
|
||||
"a=rtcp-fb:%u nack pli" RN
|
||||
"a=rtcp-fb:%u goog-remb" RN
|
||||
"a=mid:a" RN
|
||||
"a=msid:audio a" RN
|
||||
"a=ssrc:%" PRIu32 " cname:ustreamer" RN
|
||||
"a=%s" RN,
|
||||
pl, pl,
|
||||
|
||||
@@ -69,6 +69,8 @@ char *us_rtpv_make_sdp(us_rtpv_s *rtpv) {
|
||||
"a=rtcp-fb:%u nack" RN
|
||||
"a=rtcp-fb:%u nack pli" RN
|
||||
"a=rtcp-fb:%u goog-remb" RN
|
||||
"a=mid:v" RN
|
||||
"a=msid:video v" RN
|
||||
"a=ssrc:%" PRIu32 " cname:ustreamer" RN
|
||||
"a=extmap:1 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay" RN
|
||||
"a=extmap:2 urn:3gpp:video-orientation" RN
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[mypy]
|
||||
python_version = 3.9
|
||||
python_version = 3.14
|
||||
ignore_missing_imports = true
|
||||
disallow_untyped_defs = true
|
||||
strict_optional = true
|
||||
|
||||
@@ -3,7 +3,7 @@ envlist = cppcheck, flake8, pylint, mypy, vulture, htmlhint
|
||||
skipsdist = true
|
||||
|
||||
[testenv]
|
||||
basepython = python3.13
|
||||
basepython = python3.14
|
||||
changedir = /src
|
||||
|
||||
[testenv:cppcheck]
|
||||
|
||||
@@ -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.35" "January 2021"
|
||||
.TH USTREAMER-DUMP 1 "version 6.45" "January 2021"
|
||||
|
||||
.SH NAME
|
||||
ustreamer-dump \- Dump uStreamer's memory sink to 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.35" "November 2020"
|
||||
.TH USTREAMER 1 "version 6.45" "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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
|
||||
pkgname=ustreamer
|
||||
pkgver=6.35
|
||||
pkgver=6.45
|
||||
pkgrel=1
|
||||
pkgdesc="Lightweight and fast MJPEG-HTTP streamer"
|
||||
url="https://github.com/pikvm/ustreamer"
|
||||
@@ -18,7 +18,7 @@ md5sums=(SKIP)
|
||||
_options="WITH_GPIO=1 WITH_SYSTEMD=1"
|
||||
if [ -e /usr/bin/python3 ]; then
|
||||
_options="$_options WITH_PYTHON=1"
|
||||
depends+=("python>=3.13" "python<3.14")
|
||||
depends+=("python>=3.14" "python<3.15")
|
||||
makedepends+=(python-setuptools python-pip python-build python-wheel)
|
||||
fi
|
||||
if [ -e /usr/include/janus/plugins/plugin.h ];then
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=ustreamer
|
||||
PKG_VERSION:=6.35
|
||||
PKG_VERSION:=6.45
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=Maxim Devaev <mdevaev@gmail.com>
|
||||
|
||||
@@ -25,7 +25,7 @@ define Package/ustreamer
|
||||
SECTION:=multimedia
|
||||
CATEGORY:=Multimedia
|
||||
TITLE:=uStreamer
|
||||
DEPENDS:=+libpthread +libjpeg +libv4l +libbsd +libevent2 +libevent2-core +libevent2-extra +libevent2-pthreads
|
||||
DEPENDS:=+libatomic +libpthread +libjpeg +libv4l +libbsd +libevent2 +libevent2-core +libevent2-extra +libevent2-pthreads
|
||||
URL:=https://github.com/pikvm/ustreamer
|
||||
endef
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ def main() -> None:
|
||||
flags = _find_flags()
|
||||
setup(
|
||||
name="ustreamer",
|
||||
version="6.35",
|
||||
version="6.45",
|
||||
description="uStreamer tools",
|
||||
author="Maxim Devaev",
|
||||
author_email="mdevaev@gmail.com",
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
#include "threading.h"
|
||||
#include "frame.h"
|
||||
#include "xioctl.h"
|
||||
#include "tc358743.h"
|
||||
|
||||
|
||||
static const struct {
|
||||
@@ -193,12 +194,18 @@ int us_capture_open(us_capture_s *cap) {
|
||||
_LOG_DEBUG("Capture device fd=%d opened", run->fd);
|
||||
|
||||
if (cap->dv_timings && cap->persistent) {
|
||||
struct v4l2_control ctl = {.id = V4L2_CID_DV_RX_POWER_PRESENT};
|
||||
if (!us_xioctl(run->fd, VIDIOC_G_CTRL, &ctl)) {
|
||||
if (!ctl.value) {
|
||||
goto error_no_cable;
|
||||
}
|
||||
}
|
||||
_LOG_DEBUG("Probing DV-timings or QuerySTD ...");
|
||||
if (_capture_open_dv_timings(cap, false) < 0) {
|
||||
US_ONCE_FOR(run->open_error_once, __LINE__, {
|
||||
_LOG_ERROR("No signal from source");
|
||||
});
|
||||
goto error_no_signal;
|
||||
switch (_capture_open_dv_timings(cap, false)) {
|
||||
case 0: break;
|
||||
case US_ERROR_NO_SIGNAL: goto error_no_signal;
|
||||
case US_ERROR_NO_SYNC: goto error_no_sync;
|
||||
default: goto error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +223,15 @@ int us_capture_open(us_capture_s *cap) {
|
||||
if (_capture_open_format(cap, true) < 0) {
|
||||
goto error;
|
||||
}
|
||||
if (cap->dv_timings && cap->persistent) {
|
||||
struct v4l2_control ctl = {.id = TC358743_CID_LANES_ENOUGH};
|
||||
if (!us_xioctl(run->fd, VIDIOC_G_CTRL, &ctl)) {
|
||||
if (!ctl.value) {
|
||||
_LOG_ERROR("Not enough lanes, hardware can't handle this signal");
|
||||
goto error_no_lanes;
|
||||
}
|
||||
}
|
||||
}
|
||||
_capture_open_hw_fps(cap);
|
||||
_capture_open_jpeg_quality(cap);
|
||||
if (_capture_open_io_method(cap) < 0) {
|
||||
@@ -248,9 +264,23 @@ error_no_device:
|
||||
us_capture_close(cap);
|
||||
return US_ERROR_NO_DEVICE;
|
||||
|
||||
error_no_signal:
|
||||
error_no_cable:
|
||||
us_capture_close(cap);
|
||||
return US_ERROR_NO_DATA;
|
||||
return US_ERROR_NO_CABLE;
|
||||
|
||||
error_no_signal:
|
||||
US_ONCE_FOR(run->open_error_once, __LINE__, { _LOG_ERROR("No signal from source"); });
|
||||
us_capture_close(cap);
|
||||
return US_ERROR_NO_SIGNAL;
|
||||
|
||||
error_no_sync:
|
||||
US_ONCE_FOR(run->open_error_once, __LINE__, { _LOG_ERROR("No sync on signal"); });
|
||||
us_capture_close(cap);
|
||||
return US_ERROR_NO_SYNC;
|
||||
|
||||
error_no_lanes:
|
||||
us_capture_close(cap);
|
||||
return US_ERROR_NO_LANES;
|
||||
|
||||
error:
|
||||
run->open_error_once = 0;
|
||||
@@ -539,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,6 +659,10 @@ static int _capture_open_dv_timings(us_capture_s *cap, bool apply) {
|
||||
// TC358743 errors here (see in the kernel: drivers/media/i2c/tc358743.c):
|
||||
// - ENOLINK: No valid signal (SYS_STATUS & MASK_S_TMDS)
|
||||
// - ENOLCK: No sync on signal (SYS_STATUS & MASK_S_SYNC)
|
||||
switch (errno) {
|
||||
case ENOLINK: return US_ERROR_NO_SIGNAL;
|
||||
case ENOLCK: return US_ERROR_NO_SYNC;
|
||||
}
|
||||
dv_errno = errno;
|
||||
goto querystd;
|
||||
} else if (!apply) {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
|
||||
#define US_VERSION_MAJOR 6
|
||||
#define US_VERSION_MINOR 35
|
||||
#define US_VERSION_MINOR 45
|
||||
|
||||
#define US_MAKE_VERSION2(_major, _minor) #_major "." #_minor
|
||||
#define US_MAKE_VERSION1(_major, _minor) US_MAKE_VERSION2(_major, _minor)
|
||||
|
||||
@@ -378,7 +378,7 @@ int us_drm_expose_stub(us_drm_s *drm, us_drm_stub_e stub, const us_capture_s *ca
|
||||
DRAW_MSG("=== PiKVM ===\n \n< UNSUPPORTED CAPTURE FORMAT >");
|
||||
break;
|
||||
case US_DRM_STUB_NO_SIGNAL:
|
||||
DRAW_MSG("=== PiKVM ===\n \n< NO SIGNAL >");
|
||||
DRAW_MSG("=== PiKVM ===\n \n< NO LIVE VIDEO >");
|
||||
break;
|
||||
case US_DRM_STUB_BUSY:
|
||||
DRAW_MSG("=== PiKVM ===\n \n< ONLINE IS ACTIVE >");
|
||||
@@ -666,6 +666,15 @@ static drmModeModeInfo *_find_best_mode(drmModeConnector *conn, uint width, uint
|
||||
continue; // Discard interlaced
|
||||
}
|
||||
const float mode_hz = _get_refresh_rate(mode);
|
||||
if (width == 640 && height == 416 && mode->hdisplay == 640 && mode->vdisplay == 480) {
|
||||
// A special case for some ancient DOS device with VGA converter.
|
||||
// @CapnKirk in Discord
|
||||
if (hz > 0 && mode_hz < hz) {
|
||||
best = mode;
|
||||
best->vdisplay = 416;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (mode->hdisplay == width && mode->vdisplay == height) {
|
||||
best = mode; // Any mode with exact resolution
|
||||
if (hz > 0 && mode_hz == hz) {
|
||||
|
||||
@@ -24,4 +24,8 @@
|
||||
|
||||
#define US_ERROR_COMMON -1
|
||||
#define US_ERROR_NO_DEVICE -2
|
||||
#define US_ERROR_NO_DATA -3
|
||||
#define US_ERROR_NO_CABLE -3
|
||||
#define US_ERROR_NO_SIGNAL -4
|
||||
#define US_ERROR_NO_SYNC -5
|
||||
#define US_ERROR_NO_LANES -6
|
||||
#define US_ERROR_NO_DATA -7
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -121,9 +121,16 @@ void us_frametext_draw(us_frametext_s *ft, const char *text, uint width, uint he
|
||||
if (block_width == 0 || block_height == 0) {
|
||||
goto empty;
|
||||
}
|
||||
uint scale_x = frame->width / block_width / 2;
|
||||
uint scale_y = frame->height / block_height / 3;
|
||||
if (scale_x < scale_y / 1.5) {
|
||||
|
||||
// Ширина текста должна быть от 75%, до половины экрана, в зависимости от длины
|
||||
const float div_x = US_MAX(US_MIN((100 / block_width * 2), 2.0), 1.5);
|
||||
|
||||
// Высоту тоже отрегулировать как-нибудь
|
||||
const float div_y = US_MAX(US_MIN((70 / block_height * 2), 2.0), 1.5);
|
||||
|
||||
uint scale_x = frame->width / block_width / div_x;
|
||||
uint scale_y = frame->height / block_height / div_y;
|
||||
if (scale_x < scale_y / 1.5) { // Keep proportions
|
||||
scale_y = scale_x * 1.5;
|
||||
} else if (scale_y < scale_x * 1.5) {
|
||||
scale_x = scale_y / 1.5;
|
||||
|
||||
@@ -33,17 +33,6 @@
|
||||
#include "xioctl.h"
|
||||
|
||||
|
||||
#ifndef V4L2_CID_USER_TC358743_BASE
|
||||
# define V4L2_CID_USER_TC358743_BASE (V4L2_CID_USER_BASE + 0x1080)
|
||||
#endif
|
||||
#ifndef TC358743_CID_AUDIO_PRESENT
|
||||
# define TC358743_CID_AUDIO_PRESENT (V4L2_CID_USER_TC358743_BASE + 1)
|
||||
#endif
|
||||
#ifndef TC358743_CID_AUDIO_SAMPLING_RATE
|
||||
# define TC358743_CID_AUDIO_SAMPLING_RATE (V4L2_CID_USER_TC358743_BASE + 0)
|
||||
#endif
|
||||
|
||||
|
||||
int us_tc358743_xioctl_get_audio_hz(int fd, uint *audio_hz) {
|
||||
*audio_hz = 0;
|
||||
|
||||
|
||||
@@ -22,7 +22,26 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <linux/v4l2-controls.h>
|
||||
|
||||
#include "types.h"
|
||||
|
||||
|
||||
#ifndef V4L2_CID_USER_TC358743_BASE
|
||||
# define V4L2_CID_USER_TC358743_BASE (V4L2_CID_USER_BASE + 0x1080)
|
||||
#endif
|
||||
|
||||
#ifndef TC358743_CID_AUDIO_SAMPLING_RATE
|
||||
# define TC358743_CID_AUDIO_SAMPLING_RATE (V4L2_CID_USER_TC358743_BASE + 0)
|
||||
#endif
|
||||
|
||||
#ifndef TC358743_CID_AUDIO_PRESENT
|
||||
# define TC358743_CID_AUDIO_PRESENT (V4L2_CID_USER_TC358743_BASE + 1)
|
||||
#endif
|
||||
|
||||
#ifndef TC358743_CID_LANES_ENOUGH
|
||||
# define TC358743_CID_LANES_ENOUGH (V4L2_CID_USER_TC358743_BASE + 2)
|
||||
#endif
|
||||
|
||||
|
||||
int us_tc358743_xioctl_get_audio_hz(int fd, uint *audio_hz);
|
||||
|
||||
@@ -36,7 +36,7 @@ us_blank_s *us_blank_init(void) {
|
||||
blank->ft = us_frametext_init();
|
||||
blank->raw = blank->ft->frame;
|
||||
blank->jpeg = us_frame_init();
|
||||
us_blank_draw(blank, "< NO SIGNAL >", 640, 480);
|
||||
us_blank_draw(blank, "< NO LIVE VIDEO >", 640, 480);
|
||||
return blank;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -880,7 +880,7 @@ static void _http_send_snapshot(us_server_s *server) {
|
||||
if (!captured_meta.online) {
|
||||
if (blank == NULL) {
|
||||
blank = us_blank_init();
|
||||
us_blank_draw(blank, "< NO SIGNAL >", captured_meta.width, captured_meta.height);
|
||||
us_blank_draw(blank, "< NO LIVE VIDEO >", captured_meta.width, captured_meta.height);
|
||||
}
|
||||
frame = blank->jpeg;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -138,7 +139,7 @@ static const struct option _LONG_OPTS[] = {
|
||||
{"input", required_argument, NULL, _O_INPUT},
|
||||
{"resolution", required_argument, NULL, _O_RESOLUTION},
|
||||
{"format", required_argument, NULL, _O_FORMAT},
|
||||
{"format-swap-rgb", required_argument, NULL, _O_FORMAT_SWAP_RGB},
|
||||
{"format-swap-rgb", no_argument, NULL, _O_FORMAT_SWAP_RGB},
|
||||
{"tv-standard", required_argument, NULL, _O_TV_STANDARD},
|
||||
{"io-method", required_argument, NULL, _O_IO_METHOD},
|
||||
{"desired-fps", required_argument, NULL, _O_DESIRED_FPS},
|
||||
@@ -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);
|
||||
|
||||
@@ -129,7 +129,7 @@ us_stream_s *us_stream_init(us_capture_s *cap, us_encoder_s *enc) {
|
||||
|
||||
void us_stream_update_blank(us_stream_s *stream, const us_capture_s *cap) {
|
||||
us_stream_runtime_s *const run = stream->run;
|
||||
us_blank_draw(run->blank, "< NO SIGNAL >", cap->width, cap->height);
|
||||
us_blank_draw(run->blank, "< NO LIVE VIDEO >", cap->width, cap->height);
|
||||
us_fpsi_frame_to_meta(run->blank->raw, &run->notify_meta); // Initial "unchanged" meta
|
||||
_stream_update_captured_fpsi(stream, run->blank->raw, false);
|
||||
}
|
||||
@@ -529,6 +529,8 @@ static int _stream_init_loop(us_stream_s *stream) {
|
||||
|
||||
int once = 0;
|
||||
while (!atomic_load(&stream->run->stop)) {
|
||||
const char *blank_reason = "< NO LIVE VIDEO >";
|
||||
|
||||
# ifdef WITH_GPIO
|
||||
us_gpio_set_stream_online(false);
|
||||
# endif
|
||||
@@ -554,17 +556,68 @@ static int _stream_init_loop(us_stream_s *stream) {
|
||||
switch (us_capture_open(stream->cap)) {
|
||||
case 0: break;
|
||||
case US_ERROR_NO_DEVICE:
|
||||
case US_ERROR_NO_DATA:
|
||||
US_ONCE({ US_LOG_INFO("Waiting for the capture device ..."); });
|
||||
goto offline_and_retry;
|
||||
blank_reason = (
|
||||
"< NO CAPTURE DEVICE >\n \n"
|
||||
" Possible reasons: \n \n"
|
||||
" - Device unplugged \n \n"
|
||||
" - Bad config \n \n"
|
||||
" - Malfunction "
|
||||
);
|
||||
goto silent_error;
|
||||
case US_ERROR_NO_CABLE:
|
||||
blank_reason = (
|
||||
"< NO VIDEO SOURCE >\n \n"
|
||||
" Possible reasons: \n \n"
|
||||
" - Source is off \n \n"
|
||||
" - Cable problems "
|
||||
);
|
||||
goto silent_error;
|
||||
case US_ERROR_NO_SIGNAL:
|
||||
blank_reason = (
|
||||
"< NO SIGNAL DETECTED >\n \n"
|
||||
" Possible reasons: \n \n"
|
||||
" - Video suspended \n \n"
|
||||
" - Cable problems "
|
||||
);
|
||||
goto silent_error;
|
||||
case US_ERROR_NO_SYNC:
|
||||
blank_reason = (
|
||||
"< NO SYNC WITH SIGNAL >\n \n"
|
||||
" Possible reasons: \n \n"
|
||||
" - Source is crazy \n \n"
|
||||
" - Cable problems "
|
||||
);
|
||||
goto silent_error;
|
||||
case US_ERROR_NO_LANES:
|
||||
blank_reason = (
|
||||
"< UNSUPPORTED SIGNAL TIMINGS >\n \n"
|
||||
" Possible reasons: \n \n"
|
||||
" - Too high frequency \n \n"
|
||||
" - Source ignores EDID \n \n"
|
||||
" - Invalid EDID "
|
||||
);
|
||||
goto verbose_error;
|
||||
default:
|
||||
once = 0;
|
||||
goto offline_and_retry;
|
||||
goto verbose_error;
|
||||
}
|
||||
us_encoder_open(stream->enc, stream->cap);
|
||||
return 0;
|
||||
|
||||
silent_error:
|
||||
if (!stream->exit_on_device_error) {
|
||||
US_ONCE({ US_LOG_INFO("Waiting for the capture device ..."); });
|
||||
}
|
||||
goto offline_and_retry;
|
||||
|
||||
verbose_error:
|
||||
once = 0;
|
||||
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;
|
||||
@@ -577,7 +630,7 @@ static int _stream_init_loop(us_stream_s *stream) {
|
||||
width = stream->cap->width;
|
||||
height = stream->cap->height;
|
||||
}
|
||||
us_blank_draw(run->blank, "< NO SIGNAL >", width, 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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user