mirror of
https://github.com/pikvm/ustreamer.git
synced 2026-02-19 16:26:30 +00:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37216250b8 | ||
|
|
dc82894038 | ||
|
|
f3a350148e | ||
|
|
5e49f50e22 | ||
|
|
91ff97f721 | ||
|
|
daaa488dd6 | ||
|
|
bad05a6827 | ||
|
|
89d3ed98c7 | ||
|
|
1187ace2a2 | ||
|
|
9704fb054c | ||
|
|
b10148a438 | ||
|
|
1b4bcd09fe | ||
|
|
245cd94a4c | ||
|
|
8a24a0b129 | ||
|
|
b362c1fa50 | ||
|
|
0414fa5a51 | ||
|
|
191dd16d7b | ||
|
|
31a03928c9 | ||
|
|
185a8b6773 | ||
|
|
b7501cfbda | ||
|
|
1dc90dad7a | ||
|
|
45a4d148f5 | ||
|
|
93ab12b54b | ||
|
|
fb07444b70 | ||
|
|
a8ba2f7364 | ||
|
|
171bcb315a | ||
|
|
911df1af15 | ||
|
|
a165ff4523 | ||
|
|
5cfb3b1e60 | ||
|
|
82b3b78238 | ||
|
|
8a82ff6691 | ||
|
|
2807678551 | ||
|
|
e96e0aa73c | ||
|
|
d06c2619b2 | ||
|
|
6b18455d11 | ||
|
|
217977d9cb | ||
|
|
b9f186e47c | ||
|
|
d7d56f3536 | ||
|
|
2e3c764369 | ||
|
|
7236e53813 | ||
|
|
e7f7350405 | ||
|
|
81f0266a87 | ||
|
|
eb1a2e3695 | ||
|
|
6cfd3739d3 | ||
|
|
77b8386de7 | ||
|
|
a002bd3427 | ||
|
|
202b907430 | ||
|
|
d9f4aba953 | ||
|
|
1b08857534 | ||
|
|
eec19892fa | ||
|
|
40abe73391 | ||
|
|
e4f1ef654f | ||
|
|
fa6afb96ce | ||
|
|
c3f98b34f2 | ||
|
|
b7b3e8e87d | ||
|
|
94383a2d54 | ||
|
|
184a4879eb | ||
|
|
bf48908c59 | ||
|
|
66afbccf21 | ||
|
|
97dbe59aea | ||
|
|
d874fdeaec | ||
|
|
97e3938d56 | ||
|
|
87c7e8063f | ||
|
|
fe5beb0114 |
@@ -1,7 +1,7 @@
|
||||
[bumpversion]
|
||||
commit = True
|
||||
tag = True
|
||||
current_version = 3.6
|
||||
current_version = 3.21
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)(\.(?P<patch>\d+)(\-(?P<release>[a-z]+))?)?
|
||||
serialize =
|
||||
{major}.{minor}
|
||||
@@ -10,7 +10,7 @@ serialize =
|
||||
search = VERSION "{current_version}"
|
||||
replace = VERSION "{new_version}"
|
||||
|
||||
[bumpversion:file:src/python/setup.py]
|
||||
[bumpversion:file:python/setup.py]
|
||||
search = version="{current_version}"
|
||||
replace = version="{new_version}"
|
||||
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,8 +6,10 @@
|
||||
/pkg/arch/ustreamer-*.pkg.tar.xz
|
||||
/pkg/arch/ustreamer-*.pkg.tar.zst
|
||||
/build/
|
||||
/python/build/
|
||||
/config.mk
|
||||
/vgcore.*
|
||||
/ustreamer
|
||||
/ustreamer-dump
|
||||
/*.so
|
||||
/*.sock
|
||||
|
||||
45
Makefile
45
Makefile
@@ -7,7 +7,8 @@ PREFIX ?= /usr/local
|
||||
MANPREFIX ?= $(PREFIX)/share/man
|
||||
|
||||
CC ?= gcc
|
||||
CFLAGS ?= -O3 -MD
|
||||
PY ?= python3
|
||||
CFLAGS ?= -O3
|
||||
LDFLAGS ?=
|
||||
|
||||
RPI_VC_HEADERS ?= /opt/vc/include
|
||||
@@ -19,11 +20,12 @@ LINTERS_IMAGE ?= $(USTR)-linters
|
||||
|
||||
|
||||
# =====
|
||||
override CFLAGS += -c -std=c11 -Wall -Wextra -D_GNU_SOURCE
|
||||
_CFLAGS = -MD -c -std=c11 -Wall -Wextra -D_GNU_SOURCE $(CFLAGS)
|
||||
_LDFLAGS = $(LDFLAGS)
|
||||
|
||||
_COMMON_LIBS = -lm -ljpeg -pthread -lrt
|
||||
|
||||
_USTR_LIBS = $(_COMMON_LIBS) -levent -levent_pthreads -luuid
|
||||
_USTR_LIBS = $(_COMMON_LIBS) -levent -levent_pthreads
|
||||
_USTR_SRCS = $(shell ls \
|
||||
src/libs/*.c \
|
||||
src/ustreamer/*.c \
|
||||
@@ -47,7 +49,7 @@ endef
|
||||
|
||||
ifneq ($(call optbool,$(WITH_OMX)),)
|
||||
_USTR_LIBS += -lbcm_host -lvcos -lvcsm -lopenmaxil -lmmal -lmmal_core -lmmal_util -lmmal_vc_client -lmmal_components -L$(RPI_VC_LIBS)
|
||||
override CFLAGS += -DWITH_OMX -DOMX_SKIP64BIT -I$(RPI_VC_HEADERS)
|
||||
override _CFLAGS += -DWITH_OMX -DOMX_SKIP64BIT -I$(RPI_VC_HEADERS)
|
||||
_USTR_SRCS += $(shell ls \
|
||||
src/ustreamer/encoders/omx/*.c \
|
||||
src/ustreamer/h264/*.c \
|
||||
@@ -57,14 +59,14 @@ endif
|
||||
|
||||
ifneq ($(call optbool,$(WITH_GPIO)),)
|
||||
_USTR_LIBS += -lgpiod
|
||||
override CFLAGS += -DWITH_GPIO
|
||||
override _CFLAGS += -DWITH_GPIO
|
||||
_USTR_SRCS += $(shell ls src/ustreamer/gpio/*.c)
|
||||
endif
|
||||
|
||||
|
||||
WITH_PTHREAD_NP ?= 1
|
||||
ifneq ($(call optbool,$(WITH_PTHREAD_NP)),)
|
||||
override CFLAGS += -DWITH_PTHREAD_NP
|
||||
override _CFLAGS += -DWITH_PTHREAD_NP
|
||||
endif
|
||||
|
||||
|
||||
@@ -73,7 +75,7 @@ ifneq ($(call optbool,$(WITH_SETPROCTITLE)),)
|
||||
ifeq ($(shell uname -s | tr A-Z a-z),linux)
|
||||
_USTR_LIBS += -lbsd
|
||||
endif
|
||||
override CFLAGS += -DWITH_SETPROCTITLE
|
||||
override _CFLAGS += -DWITH_SETPROCTITLE
|
||||
endif
|
||||
|
||||
|
||||
@@ -81,16 +83,16 @@ endif
|
||||
all: $(USTR) $(DUMP) python
|
||||
|
||||
|
||||
install: $(USTR) $(DUMP)
|
||||
install: all
|
||||
mkdir -p $(DESTDIR)$(PREFIX)/bin $(DESTDIR)$(MANPREFIX)/man1
|
||||
install -m755 $(USTR) $(DESTDIR)$(PREFIX)/bin/$(USTR)
|
||||
install -m755 $(DUMP) $(DESTDIR)$(PREFIX)/bin/$(DUMP)
|
||||
install -m644 man/$(USTR).1 $(DESTDIR)$(MANPREFIX)/man1/$(USTR).1
|
||||
install -m644 man/$(DUMP).1 $(DESTDIR)$(MANPREFIX)/man1/$(DUMP).1
|
||||
gzip $(DESTDIR)$(MANPREFIX)/man1/$(USTR).1
|
||||
gzip $(DESTDIR)$(MANPREFIX)/man1/$(DUMP).1
|
||||
gzip -f $(DESTDIR)$(MANPREFIX)/man1/$(USTR).1
|
||||
gzip -f $(DESTDIR)$(MANPREFIX)/man1/$(DUMP).1
|
||||
ifneq ($(call optbool,$(WITH_PYTHON)),)
|
||||
cd src/python && python3 setup.py install --prefix=$(PREFIX) --root=$(DESTDIR)
|
||||
cd python && $(PY) setup.py install --prefix=$(PREFIX) --root=$(if $(DESTDIR),$(DESTDIR),/)
|
||||
endif
|
||||
|
||||
|
||||
@@ -107,33 +109,34 @@ regen:
|
||||
$(USTR): $(_USTR_SRCS:%.c=$(BUILD)/%.o)
|
||||
# $(info ========================================)
|
||||
$(info == LD $@)
|
||||
@ $(CC) $^ -o $@ $(LDFLAGS) $(_USTR_LIBS)
|
||||
@ $(CC) $^ -o $@ $(_LDFLAGS) $(_USTR_LIBS)
|
||||
# $(info :: CC = $(CC))
|
||||
# $(info :: LIBS = $(_USTR_LIBS))
|
||||
# $(info :: CFLAGS = $(CFLAGS))
|
||||
# $(info :: LDFLAGS = $(LDFLAGS))
|
||||
# $(info :: CFLAGS = $(_CFLAGS))
|
||||
# $(info :: LDFLAGS = $(_LDFLAGS))
|
||||
|
||||
|
||||
$(DUMP): $(_DUMP_SRCS:%.c=$(BUILD)/%.o)
|
||||
# $(info ========================================)
|
||||
$(info == LD $@)
|
||||
@ $(CC) $^ -o $@ $(LDFLAGS) $(_DUMP_LIBS)
|
||||
@ $(CC) $^ -o $@ $(_LDFLAGS) $(_DUMP_LIBS)
|
||||
# $(info :: CC = $(CC))
|
||||
# $(info :: LIBS = $(_DUMP_LIBS))
|
||||
# $(info :: CFLAGS = $(CFLAGS))
|
||||
# $(info :: LDFLAGS = $(LDFLAGS))
|
||||
# $(info :: CFLAGS = $(_CFLAGS))
|
||||
# $(info :: LDFLAGS = $(_LDFLAGS))
|
||||
|
||||
|
||||
$(BUILD)/%.o: %.c
|
||||
$(info -- CC $<)
|
||||
@ mkdir -p $(dir $@) || true
|
||||
@ $(CC) $< -o $@ $(CFLAGS)
|
||||
@ $(CC) $< -o $@ $(_CFLAGS)
|
||||
|
||||
|
||||
python:
|
||||
ifneq ($(call optbool,$(WITH_PYTHON)),)
|
||||
cd src/python && python3 setup.py build
|
||||
ln -sf src/python/build/lib.*/*.so .
|
||||
$(info == PY_BUILD ustreamer-*.so)
|
||||
@ cd python && $(PY) setup.py build
|
||||
@ ln -sf python/build/lib.*/*.so .
|
||||
else
|
||||
@ true
|
||||
endif
|
||||
@@ -180,7 +183,7 @@ clean-all: linters clean
|
||||
-it $(LINTERS_IMAGE) bash -c "cd src && rm -rf linters/{.tox,.mypy_cache}"
|
||||
clean:
|
||||
rm -rf pkg/arch/pkg pkg/arch/src pkg/arch/v*.tar.gz pkg/arch/ustreamer-*.pkg.tar.{xz,zst}
|
||||
rm -rf $(USTR) $(DUMP) $(BUILD) src/python/build vgcore.* *.sock *.so
|
||||
rm -rf $(USTR) $(DUMP) $(BUILD) python/build vgcore.* *.sock *.so
|
||||
|
||||
|
||||
.PHONY: python linters
|
||||
|
||||
62
README.md
62
README.md
@@ -11,21 +11,21 @@
|
||||
|
||||
| **Feature** | **µStreamer** | **mjpg-streamer** |
|
||||
|----------|---------------|-------------------|
|
||||
| Multithreaded JPEG encoding |  Yes |  No |
|
||||
| [OpenMAX IL](https://www.khronos.org/openmaxil) hardware acceleration<br>on Raspberry Pi |  Yes |  No |
|
||||
| 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> |
|
||||
| [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 |  Yes |  Partially yes <sup>1</sup> |
|
||||
| Option to skip frames when streaming<br>static images by HTTP to save the traffic |  Yes <sup>2</sup> |  No |
|
||||
| Streaming via UNIX domain socket |  Yes |  No |
|
||||
| Debug logs without recompiling,<br>performance statistics log,<br>access to HTTP streaming parameters |  Yes |  No |
|
||||
| Option to serve files<br>with a built-in HTTP server |  Yes |  Regular files only |
|
||||
| Signaling about the stream state<br>on GPIO using [libgpiod](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/about) |  Yes |  No |
|
||||
| Access to webcam controls (focus, servos)<br>and settings such as brightness via HTTP |  No |  Yes |
|
||||
| Compatibility with mjpg-streamer's API |  Yes | :) |
|
||||
| Multithreaded JPEG encoding | ✔ | ✘ |
|
||||
| [OpenMAX IL](https://www.khronos.org/openmaxil) hardware acceleration<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> |
|
||||
| [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 | ✔ | ✘ |
|
||||
| Debug logs without recompiling,<br>performance statistics log,<br>access to HTTP streaming parameters | ✔ | ✘ |
|
||||
| Option to serve files<br>with a built-in HTTP server | ✔ | ☹ Regular files only |
|
||||
| Signaling about the stream state<br>on GPIO using [libgpiod](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/about) | ✔ | ✘ |
|
||||
| Access to webcam controls (focus, servos)<br>and settings such as brightness via HTTP | ✘ | ✔ |
|
||||
| Compatibility with mjpg-streamer's API | ✔ | :) |
|
||||
|
||||
Footnotes:
|
||||
* ```1``` Long before µStreamer, I made a [patch](https://github.com/jacksonliam/mjpg-streamer/pull/164) to add DV-timings support to mjpg-streamer and to keep it from hanging up no device disconnection. Alas, the patch is far from perfect and I can't guarantee it will work every time - mjpg-streamer's source code is very complicated and its structure is hard to understand. With this in mind, along with needing multithreading and JPEG hardware acceleration in the future, I decided to make my own stream server from scratch instead of supporting legacy code.
|
||||
|
||||
|
||||
* ```2``` This feature allows to cut down outgoing traffic several-fold when streaming HDMI, but it increases CPU usage a little bit. The idea is that HDMI is a fully digital interface and each captured frame can be identical to the previous one byte-wise. There's no need to stream the same image over the net several times a second. With the `--drop-same-frames=20` option enabled, µStreamer will drop all the matching frames (with a limit of 20 in a row). Each new frame is matched with the previous one first by length, then using ```memcmp()```.
|
||||
|
||||
-----
|
||||
@@ -34,12 +34,12 @@ If you're going to live-stream from your backyard webcam and need to control it,
|
||||
|
||||
-----
|
||||
# Building
|
||||
You'll need ```make```, ```gcc```, ```libevent``` with ```pthreads``` support, ```libjpeg8```/```libjpeg-turbo```, ```libuuid``` and ```libbsd``` (only for Linux).
|
||||
You'll need ```make```, ```gcc```, ```libevent``` with ```pthreads``` support, ```libjpeg8```/```libjpeg-turbo``` and ```libbsd``` (only for Linux).
|
||||
|
||||
* Arch: `sudo pacman -S libevent libjpeg-turbo libutil-linux libbsd`.
|
||||
* Raspbian: `sudo apt install libevent-dev libjpeg8-dev uuid-dev libbsd-dev`. Add `libraspberrypi-dev` for `WITH_OMX=1` and `libgpiod` for `WITH_GPIO=1`.
|
||||
* Debian: `sudo apt install build-essential libevent-dev libjpeg62-turbo-dev uuid-dev libbsd-dev`.
|
||||
* Ubuntu 20.04 x86_64: `sudo apt install build-essential libevent-dev libjpeg62-dev uuid-dev libbsd-dev make gcc libjpeg8 libjpeg-turbo8 libuuid1 libbsd0`.
|
||||
* Raspbian: `sudo apt install libevent-dev libjpeg8-dev libbsd-dev`. Add `libraspberrypi-dev` for `WITH_OMX=1` and `libgpiod` for `WITH_GPIO=1`.
|
||||
* Debian: `sudo apt install build-essential libevent-dev libjpeg62-turbo-dev libbsd-dev`.
|
||||
* Ubuntu 20.04 x86_64: `sudo apt install build-essential libevent-dev libjpeg-dev libjpeg62-dev libbsd-dev make gcc libjpeg8 libjpeg-turbo8 libbsd0`.
|
||||
|
||||
On Raspberry Pi you can build the program with OpenMAX IL. To do this pass option ```WITH_OMX=1``` to ```make```. To enable GPIO support install [libgpiod](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/about) and pass option ```WITH_GPIO=1```. If the compiler reports about a missing function ```pthread_get_name_np()``` (or similar), add option ```WITH_PTHREAD_NP=0``` (it's enabled by default). For the similar error with ```setproctitle()``` add option ```WITH_SETPROCTITLE=0```.
|
||||
|
||||
@@ -50,7 +50,7 @@ $ make
|
||||
$ ./ustreamer --help
|
||||
```
|
||||
|
||||
AUR has a package for Arch Linux: https://aur.archlinux.org/packages/ustreamer. It should compile automatically with OpenMAX IL on Raspberry Pi, if the corresponding headers are present in ```/opt/vc/include```.
|
||||
AUR has a package for Arch Linux: https://aur.archlinux.org/packages/ustreamer. It should compile automatically with OpenMAX IL on Raspberry Pi, if the corresponding headers are present in ```/opt/vc/include```.
|
||||
FreeBSD port: https://www.freshports.org/multimedia/ustreamer.
|
||||
|
||||
-----
|
||||
@@ -77,6 +77,34 @@ $ ./ustreamer \
|
||||
|
||||
You can always view the full list of options with ```ustreamer --help```.
|
||||
|
||||
-----
|
||||
# Raspberry Pi Camera Example
|
||||
Example usage for the Raspberry Pi v1 camera:
|
||||
```bash
|
||||
$ sudo modprobe bcm2835-v4l2
|
||||
$ ./ustreamer --host :: -m jpeg --device-timeout=5 --buffers=3 -r 2592x1944
|
||||
```
|
||||
|
||||
:exclamation: Please note that newer camera models have a different maximum resolution. You can see the supported resolutions at the [PiCamera documentation](https://picamera.readthedocs.io/en/release-1.13/fov.html#sensor-modes).
|
||||
|
||||
:exclamation: If you get a poor framerate, it could be that the camera is switched to photo mode, which produces a low framerate (but a higher quality picture). This is because `bcm2835-v4l2` switches to photo mode at resolutions higher than `1280x720`. To work around this, pass the `max_video_width` and `max_video_height` module parameters like so:
|
||||
|
||||
```bash
|
||||
$ modprobe bcm2835-v4l2 max_video_width=2592 max_video_height=1944
|
||||
```
|
||||
|
||||
-----
|
||||
# Tips & tricks for v4l2
|
||||
v4l2 utilities provide the tools to manage USB webcam setting and information. Scripts can be use to make adjustments and run manually or with cron. Running in cron for example to change the exposure settings at certain times of day. The package is available in all Linux distributions and is usually called `v4l-utils`.
|
||||
|
||||
* List of available video devices: `v4l2-ctl --list-devices`.
|
||||
* List available control settings: `v4l2-ctl -d /dev/video0 --list-ctrls`.
|
||||
* List available video formats: `v4l2-ctl -d /dev/video0 --list-formats-ext`.
|
||||
* Read the current setting: `v4l2-ctl d /dev/video0 --get-ctrl=exposure_auto`.
|
||||
* Change the setting value: `v4l2-ctl d /dev/video0 --set-ctrl=exposure_auto=1`.
|
||||
|
||||
[Here](https://www.kurokesu.com/main/2016/01/16/manual-usb-camera-settings-in-linux/) you can find more examples. Documentation is available in [`man v4l2-ctl`](https://www.mankier.com/1/v4l2-ctl).
|
||||
|
||||
-----
|
||||
# See also
|
||||
* [Running uStreamer via systemd service](https://github.com/pikvm/ustreamer/issues/16).
|
||||
|
||||
60
README.ru.md
60
README.ru.md
@@ -11,21 +11,21 @@
|
||||
|
||||
| **Фича** | **µStreamer** | **mjpg-streamer** |
|
||||
|----------|---------------|-------------------|
|
||||
| Многопоточное кодирование JPEG |  Есть |  Нет |
|
||||
| Аппаратное кодирование с помощью [OpenMAX IL](https://www.khronos.org/openmaxil) на Raspberry Pi |  Есть |  Нет |
|
||||
| Поведение при физическом отключении<br>устройства от сервера во время работы |  Транслирует черный экран<br>с надписью ```NO SIGNAL```,<br>пока устройство не будет подключено снова |  Прерывает трансляцию <sup>1</sup> |
|
||||
| Поддержка [DV-таймингов](https://linuxtv.org/downloads/v4l-dvb-apis-new/userspace-api/v4l/dv-timings.html) - возможности<br>изменения параметров разрешения<br>трансляции на лету по сигналу<br>источника (устройства видеозахвата) |  Есть |  Условно есть <sup>1</sup> |
|
||||
| Возможность пропуска фреймов при передаче<br>статического изображения по HTTP<br>для экономии трафика |  Есть <sup>2</sup> |  Нет |
|
||||
| Стрим через UNIX domain socket |  Есть |  Нет |
|
||||
| Дебаг-логи без перекомпиляции,<br>логгирование статистики производительности,<br>возможность получения параметров<br>трансляции по HTTP |  Есть |  Нет |
|
||||
| Возможность сервить файлы встроенным<br>HTTP-сервером |  Есть |  Нет каталогов |
|
||||
| Вывод сигналов о состоянии стрима на GPIO<br>с помощью [libgpiod](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/about) |  Есть |  Нет |
|
||||
| Поддержка контролов веб-камер (фокус,<br> движение сервами) и всяких настроек,<br> типа яркости, через HTTP |  Нет |  Есть |
|
||||
| Совместимость с API mjpg-streamer'а |  Есть | :) |
|
||||
| Многопоточное кодирование JPEG | ✔ | ✘ |
|
||||
| Аппаратное кодирование с помощью [OpenMAX IL](https://www.khronos.org/openmaxil) на Raspberry Pi | ✔ | ✘ |
|
||||
| Поведение при физическом отключении<br>устройства от сервера во время работы | ✔ Транслирует черный экран<br>с надписью ```NO SIGNAL```,<br>пока устройство не будет подключено снова | ✘ Прерывает трансляцию <sup>1</sup> |
|
||||
| Поддержка [DV-таймингов](https://linuxtv.org/downloads/v4l-dvb-apis-new/userspace-api/v4l/dv-timings.html) - возможности<br>изменения параметров разрешения<br>трансляции на лету по сигналу<br>источника (устройства видеозахвата) | ✔ | ☹ Условно есть <sup>1</sup> |
|
||||
| Возможность пропуска фреймов при передаче<br>статического изображения по HTTP<br>для экономии трафика | ✔ <sup>2</sup> | ✘ |
|
||||
| Стрим через UNIX domain socket | ✔ | ✘ |
|
||||
| Дебаг-логи без перекомпиляции,<br>логгирование статистики производительности,<br>возможность получения параметров<br>трансляции по HTTP | ✔ | ✘ |
|
||||
| Возможность сервить файлы встроенным<br>HTTP-сервером | ✔ | ☹ Нет каталогов |
|
||||
| Вывод сигналов о состоянии стрима на GPIO<br>с помощью [libgpiod](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/about) | ✔ | ✘ |
|
||||
| Поддержка контролов веб-камер (фокус,<br> движение сервами) и всяких настроек,<br> типа яркости, через HTTP | ✘ | ✔ |
|
||||
| Совместимость с API mjpg-streamer'а | ✔ | :) |
|
||||
|
||||
Сносочки:
|
||||
* ```1``` Еще до написания µStreamer, я запилил [патч](https://github.com/jacksonliam/mjpg-streamer/pull/164), добавляющий в mjpg-streamer поддержку DV-таймингов и предотвращающий его зависание при отключении устройства. Однако патч, увы, далек от совершенства и я не гарантирую его стопроцентную работоспособность, поскольку код mjpg-streamer чрезвычайно запутан и очень плохо структурирован. Учитывая это, а также то, что в дальнейшем мне потребовались многопоточность и аппаратное кодирование JPEG, было принято решение написать свой стрим-сервер с нуля, чтобы не тратить силы на поддержку лишнего легаси.
|
||||
|
||||
|
||||
* ```2``` Это фича позволяет в несколько раз снизить объем исходящего трафика при трансляции HDMI, однако немного увеличивает загрузку процессора. Суть в том, что HDMI - полностью цифровой интерфейс, и новый захваченный фрейм может быть идентичен предыдущему в точности до байта. В этом случае нет нужды передавать одну и ту же картинку по сети несколько раз в секунду. При использовании опции `--drop-same-frames=20`, µStreamer будет дропать все одинаковые фреймы, но не более 20 подряд. Новый фрейм сравнивается с предыдущим сначала по длине, а затем помощью ```memcmp()```.
|
||||
|
||||
-----
|
||||
@@ -34,12 +34,12 @@
|
||||
|
||||
-----
|
||||
# Сборка
|
||||
Для сборки вам понадобятся ```make```, ```gcc```, ```libevent``` с поддержкой ```pthreads```, ```libjpeg8```/```libjpeg-turbo```, ```libuuid``` и ```libbsd``` (только для Linux).
|
||||
Для сборки вам понадобятся ```make```, ```gcc```, ```libevent``` с поддержкой ```pthreads```, ```libjpeg8```/```libjpeg-turbo``` и ```libbsd``` (только для Linux).
|
||||
|
||||
* Arch: `sudo pacman -S libevent libjpeg-turbo libutil-linux libbsd`.
|
||||
* Raspbian: `sudo apt install libevent-dev libjpeg8-dev uuid-dev libbsd-dev`. Добавьте `libraspberrypi-dev` для сборки с `WITH_OMX=1` и `libgpiod` для `WITH_GPIO=1`.
|
||||
* Debian: `sudo apt install build-essential libevent-dev libjpeg62-turbo-dev uuid-dev libbsd-dev`.
|
||||
* Ubuntu 20.04 x86_64: `sudo apt install build-essential libevent-dev libjpeg62-dev uuid-dev libbsd-dev make gcc libjpeg8 libjpeg-turbo8 libuuid1 libbsd0`.
|
||||
* Raspbian: `sudo apt install libevent-dev libjpeg8-dev libbsd-dev`. Добавьте `libraspberrypi-dev` для сборки с `WITH_OMX=1` и `libgpiod` для `WITH_GPIO=1`.
|
||||
* Debian: `sudo apt install build-essential libevent-dev libjpeg62-turbo-dev libbsd-dev`.
|
||||
* Ubuntu 20.04 x86_64: `sudo apt install build-essential libevent-dev libjpeg-dev libjpeg62-dev libbsd-dev make gcc libjpeg8 libjpeg-turbo8 libbsd0`.
|
||||
|
||||
На Raspberry Pi программу можно собрать с поддержкой OpenMAX IL. Для этого передайте ```make``` параметр ```WITH_OMX=1```. Для включения сборки с поддержкой GPIO установите [libgpiod](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/about) и добавьте параметр ```WITH_GPIO=1```. Если при сборке компилятор ругается на отсутствие функции ```pthread_get_name_np()``` или другой подобной, добавьте параметр ```WITH_PTHREAD_NP=0``` (по умолчанию он включен). При аналогичной ошибке с функцией ```setproctitle()``` добавьте параметр ```WITH_SETPROCTITLE=0```.
|
||||
|
||||
@@ -77,6 +77,34 @@ $ ./ustreamer \
|
||||
|
||||
За полным списком опций обращайтесь ко встроенной справке: ```ustreamer --help```.
|
||||
|
||||
-----
|
||||
# Камера Raspberry Pi
|
||||
Пример использования камеры Raspberry Pi v1:
|
||||
```bash
|
||||
$ sudo modprobe bcm2835-v4l2
|
||||
$ ./ustreamer --host :: -m jpeg --device-timeout=5 --buffers=3 -r 2592x1944
|
||||
```
|
||||
|
||||
:exclamation: Обратите внимание что боле новые модели камеры имеют другое максимальное разрешение. Список поддерживаемых разрешений можно найти в [документации PiCamera](https://picamera.readthedocs.io/en/release-1.13/fov.html#sensor-modes).
|
||||
|
||||
:exclamation: Если камера выдает низкий фреймрейт, возможно что она работает в фото-режиме, где производит более низкий фпс, но более качественную кратинку. Это происходит потому что `bcm2835-v4l2` переключает камеру в фото-режим на разрешениях выше `1280x720`. Чтобы обойти это, передайте параметры `max_video_width` и `max_video_height` при загрузке модуля, например:
|
||||
|
||||
```bash
|
||||
$ modprobe bcm2835-v4l2 max_video_width=2592 max_video_height=1944
|
||||
```
|
||||
|
||||
-----
|
||||
# Утилиты V4L2
|
||||
V4L2 предоставляет ряд официальных утилит для управления USB-вебкамерами и получения информации об устройствах. С их помощью можно писать всякие настроечные скрипты и запускать их по крону, если, например, вам требуется изменять настройки экспозиции в зависимости от времени суток. Пакет с этими утилитами доступен на всех дистрибутивах Linux и обычно называется `v4l-utils`.
|
||||
|
||||
* Вывести список видеоустройств: `v4l2-ctl --list-devices`.
|
||||
* Вывести список доступных контролов устройства: `v4l2-ctl -d /dev/video0 --list-ctrls`.
|
||||
* Вывести список доступных форматов видео: `v4l2-ctl -d /dev/video0 --list-formats-ext`.
|
||||
* Показать текущее значение контрола: `v4l2-ctl d /dev/video0 --get-ctrl=exposure_auto`.
|
||||
* Изменить значение контрола: `v4l2-ctl d /dev/video0 --set-ctrl=exposure_auto=1`.
|
||||
|
||||
Больше примеров вы можете найти [здесь](https://www.kurokesu.com/main/2016/01/16/manual-usb-camera-settings-in-linux/), а документацию в [`man v4l2-ctl`](https://www.mankier.com/1/v4l2-ctl).
|
||||
|
||||
-----
|
||||
# Смотрите также
|
||||
* [Запуск с помощью systemd-сервиса](https://github.com/pikvm/ustreamer/issues/16).
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
FROM archlinux/base
|
||||
FROM archlinux/archlinux:base-devel
|
||||
|
||||
RUN mkdir -p /etc/pacman.d/hooks \
|
||||
&& ln -s /dev/null /etc/pacman.d/hooks/30-systemd-tmpfiles.hook
|
||||
|
||||
RUN echo "Server = http://mirror.yandex.ru/archlinux/\$repo/os/\$arch" > /etc/pacman.d/mirrorlist
|
||||
|
||||
RUN pacman -Syu --noconfirm \
|
||||
&& pacman -S --needed --noconfirm \
|
||||
base \
|
||||
base-devel \
|
||||
vim \
|
||||
git \
|
||||
libjpeg \
|
||||
@@ -17,7 +18,8 @@ RUN pacman -Syu --noconfirm \
|
||||
python-tox \
|
||||
cppcheck \
|
||||
npm \
|
||||
&& (pacman -Sc --noconfirm || true)
|
||||
&& (pacman -Sc --noconfirm || true) \
|
||||
&& rm -rf /var/cache/pacman/pkg/*
|
||||
|
||||
RUN npm install htmlhint -g
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
[tox]
|
||||
envlist = cppcheck, flake8, pylint, mypy, vulture, htmlhint
|
||||
envlist = cppcheck-src, cppcheck-python, flake8, pylint, mypy, vulture, htmlhint
|
||||
skipsdist = true
|
||||
|
||||
[testenv]
|
||||
basepython = python3.9
|
||||
changedir = /src
|
||||
|
||||
[testenv:cppcheck]
|
||||
[testenv:cppcheck-src]
|
||||
whitelist_externals = cppcheck
|
||||
commands = cppcheck \
|
||||
--force \
|
||||
@@ -17,33 +17,49 @@ commands = cppcheck \
|
||||
--suppress=assignmentInAssert \
|
||||
--suppress=variableScope \
|
||||
--inline-suppr \
|
||||
--library=python \
|
||||
-DCHAR_BIT=8 \
|
||||
-DWITH_OMX \
|
||||
-DWITH_GPIO \
|
||||
src
|
||||
src python
|
||||
|
||||
[testenv:cppcheck-python]
|
||||
whitelist_externals = cppcheck
|
||||
commands = cppcheck \
|
||||
--force \
|
||||
--std=c11 \
|
||||
--error-exitcode=1 \
|
||||
--quiet \
|
||||
--enable=warning,unusedFunction,portability,performance,style \
|
||||
--suppress=assignmentInAssert \
|
||||
--suppress=variableScope \
|
||||
--inline-suppr \
|
||||
--library=python \
|
||||
-DCHAR_BIT=8 \
|
||||
python
|
||||
|
||||
[testenv:flake8]
|
||||
whitelist_externals = bash
|
||||
commands = bash -c 'flake8 --config=linters/flake8.ini tools/*.py' src/python/*.py
|
||||
commands = bash -c 'flake8 --config=linters/flake8.ini tools/*.py' python/*.py
|
||||
deps =
|
||||
flake8
|
||||
flake8-quotes
|
||||
|
||||
[testenv:pylint]
|
||||
whitelist_externals = bash
|
||||
commands = bash -c 'pylint --rcfile=linters/pylint.ini --output-format=colorized --reports=no tools/*.py src/python/*.py'
|
||||
commands = bash -c 'pylint --rcfile=linters/pylint.ini --output-format=colorized --reports=no tools/*.py python/*.py'
|
||||
deps =
|
||||
pylint
|
||||
|
||||
[testenv:mypy]
|
||||
whitelist_externals = bash
|
||||
commands = bash -c 'mypy --config-file=linters/mypy.ini tools/*.py src/python/*.py'
|
||||
commands = bash -c 'mypy --config-file=linters/mypy.ini tools/*.py python/*.py'
|
||||
deps =
|
||||
mypy
|
||||
|
||||
[testenv:vulture]
|
||||
whitelist_externals = bash
|
||||
commands = bash -c 'vulture tools/*.py src/python/*.py'
|
||||
commands = bash -c 'vulture tools/*.py python/*.py'
|
||||
deps =
|
||||
vulture
|
||||
|
||||
|
||||
@@ -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 3.6" "January 2021"
|
||||
.TH USTREAMER-DUMP 1 "version 3.21" "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 3.6" "November 2020"
|
||||
.TH USTREAMER 1 "version 3.21" "November 2020"
|
||||
|
||||
.SH NAME
|
||||
ustreamer \- stream MJPG video from any V4L2 device to the network
|
||||
@@ -104,7 +104,7 @@ It doesn't do anything. Still here for compatibility. Required \fBWITH_OMX\fR fe
|
||||
Path to JPEG file that will be shown when the device is disconnected during the streaming. Default: black screen 640x480 with 'NO SIGNAL'.
|
||||
.TP
|
||||
.BR \-K\ \fIsec ", " \-\-last\-as\-blank\ \fIsec
|
||||
Show the last frame received from the camera after it was disconnected, but no more than specified time (or endlessly if 0 is specified). If the device has not yet been online, display 'NO SIGNAL' or the image specified by option \-\-blank. Default: disabled.
|
||||
Show the last frame received from the camera after it was disconnected, but no more than specified time (or endlessly if 0 is specified). If the device has not yet been online, display 'NO SIGNAL' or the image specified by option \-\-blank. Note: currently this option has no effect on memory sinks. Default: disabled.
|
||||
.TP
|
||||
.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.
|
||||
@@ -118,7 +118,7 @@ Delay before trying to connect to the device again after an error (timeout for e
|
||||
.SS "Image control options"
|
||||
.TP
|
||||
.BR \-\-image\-default
|
||||
Reset all image settings bellow to default. Default: no change.
|
||||
Reset all image settings below to default. Default: no change.
|
||||
.TP
|
||||
.BR \-\-brightness\ \fIN ", " \fIauto ", " \fIdefault
|
||||
Set brightness. Default: no change.
|
||||
@@ -166,8 +166,8 @@ Bind to this TCP port. Default: 8080.
|
||||
.TP
|
||||
.BR \-U\ \fIpath ", " \-\-unix\ \fIpath
|
||||
Bind to UNIX domain socket. Default: disabled.
|
||||
.tp
|
||||
.br \-d ", " \-\-unix\-rm
|
||||
.TP
|
||||
.BR \-d ", " \-\-unix\-rm
|
||||
Try to remove old unix socket file before binding. default: disabled.
|
||||
.TP
|
||||
.BR \-M\ \fImode ", " \-\-unix\-mode\ \fImode
|
||||
@@ -234,6 +234,12 @@ Client TTL. Default: 10.
|
||||
.TP
|
||||
.BR \-\-h264\-sink\-timeout\ \fIsec
|
||||
Timeout for lock. Default: 1.
|
||||
.TP
|
||||
.BR \-\-h264\-bitrate\ \fIkbps
|
||||
H264 bitrate in Kbps. Default: 5000.
|
||||
.TP
|
||||
.BR \-\-h264\-gop\ \fIN
|
||||
Intarval between keyframes. Default: 30.
|
||||
|
||||
.SS "Process options"
|
||||
.TP
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
|
||||
|
||||
pkgname=ustreamer
|
||||
pkgver=3.6
|
||||
pkgver=3.21
|
||||
pkgrel=1
|
||||
pkgdesc="Lightweight and fast MJPG-HTTP streamer"
|
||||
url="https://github.com/pikvm/ustreamer"
|
||||
license=(GPL)
|
||||
arch=(i686 x86_64 armv6h armv7h aarch64)
|
||||
depends=(libjpeg libevent libutil-linux libbsd libgpiod)
|
||||
# optional: raspberrypi-firmware for OMX encoder
|
||||
depends=(libjpeg libevent libbsd libgpiod)
|
||||
makedepends=(gcc make)
|
||||
source=(${pkgname}::"git+https://github.com/pikvm/ustreamer#commit=v${pkgver}")
|
||||
md5sums=(SKIP)
|
||||
|
||||
|
||||
_options="WITH_GPIO=1"
|
||||
if [ -e /usr/lib/python3 ]; then
|
||||
if [ -e /usr/bin/python3 ]; then
|
||||
_options="$_options WITH_PYTHON=1"
|
||||
depends+=(python)
|
||||
makedepends+=(python-setuptools)
|
||||
@@ -28,20 +28,21 @@ if [ -e /opt/vc/include/IL/OMX_Core.h ]; then
|
||||
fi
|
||||
|
||||
|
||||
# LD does not link mmal with this option
|
||||
# This DOESN'T affect setup.py
|
||||
LDFLAGS="${LDFLAGS//--as-needed/}"
|
||||
export LDFLAGS="${LDFLAGS//,,/,}"
|
||||
|
||||
|
||||
build() {
|
||||
cd "$srcdir"
|
||||
rm -rf $pkgname-build
|
||||
cp -r $pkgname $pkgname-build
|
||||
cd $pkgname-build
|
||||
|
||||
# LD does not link mmal with this option
|
||||
LDFLAGS="${LDFLAGS//--as-needed/}"
|
||||
LDFLAGS="${LDFLAGS//,,/,}"
|
||||
|
||||
make $_options CFLAGS="$CFLAGS" LDFLAGS="$LDFLAGS" $MAKEFLAGS
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$srcdir/$pkgname-build"
|
||||
make DESTDIR="$pkgdir" PREFIX=/usr install
|
||||
make $_options CFLAGS="$CFLAGS" LDFLAGS="$LDFLAGS" DESTDIR="$pkgdir" PREFIX=/usr install
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ RUN apt-get update \
|
||||
libevent-2.1 \
|
||||
libevent-pthreads-2.1-6 \
|
||||
libjpeg8 \
|
||||
uuid \
|
||||
libbsd0 \
|
||||
libgpiod2 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -20,7 +20,6 @@ RUN apt-get update \
|
||||
libevent-2.1 \
|
||||
libevent-pthreads-2.1-6 \
|
||||
libjpeg8 \
|
||||
uuid \
|
||||
libbsd0 \
|
||||
libgpiod2 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -8,7 +8,6 @@ RUN apt-get update \
|
||||
git \
|
||||
libevent-dev \
|
||||
libjpeg62-turbo-dev \
|
||||
uuid-dev \
|
||||
libbsd-dev \
|
||||
libgpiod-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -25,7 +24,6 @@ RUN apt-get update \
|
||||
libevent-2.1 \
|
||||
libevent-pthreads-2.1-6 \
|
||||
libjpeg62-turbo \
|
||||
uuid \
|
||||
libbsd0 \
|
||||
libgpiod2 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -17,7 +17,6 @@ IUSE=""
|
||||
DEPEND="
|
||||
>=dev-libs/libevent-2.1.8
|
||||
>=media-libs/libjpeg-turbo-1.5.3
|
||||
>=sys-apps/util-linux-2.33
|
||||
>=dev-libs/libbsd-0.9.1
|
||||
"
|
||||
RDEPEND="${DEPEND}"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=ustreamer
|
||||
PKG_VERSION:=3.6
|
||||
PKG_VERSION:=3.21
|
||||
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 +libuuid +libbsd +libevent2 +libevent2-core +libevent2-extra +libevent2-pthreads
|
||||
DEPENDS:=+libpthread +libjpeg +libv4l +libbsd +libevent2 +libevent2-core +libevent2-extra +libevent2-pthreads
|
||||
URL:=https://github.com/pikvm/ustreamer
|
||||
endef
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from distutils.core import setup
|
||||
if __name__ == "__main__":
|
||||
setup(
|
||||
name="ustreamer",
|
||||
version="3.6",
|
||||
version="3.21",
|
||||
description="uStreamer tools",
|
||||
author="Maxim Devaev",
|
||||
author_email="mdevaev@gmail.com",
|
||||
@@ -17,6 +17,10 @@ if __name__ == "__main__":
|
||||
libraries=["rt", "m", "pthread"],
|
||||
undef_macros=["NDEBUG"],
|
||||
sources=["ustreamer.c"],
|
||||
depends=[
|
||||
"../src/libs/tools.h",
|
||||
"../src/libs/memsinksh.h",
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -2,9 +2,9 @@
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/file.h>
|
||||
@@ -13,25 +13,50 @@
|
||||
|
||||
#include <Python.h>
|
||||
|
||||
#include "../libs/tools.h" // Just a header without C-sources
|
||||
#include "../libs/memsinksh.h" // No sources again
|
||||
#include "../src/libs/tools.h" // Just a header without C-sources
|
||||
#include "../src/libs/memsinksh.h" // No sources again
|
||||
|
||||
|
||||
typedef struct {
|
||||
uint64_t id;
|
||||
long double ts;
|
||||
|
||||
uint8_t *data;
|
||||
size_t used;
|
||||
size_t allocated;
|
||||
|
||||
unsigned width;
|
||||
unsigned height;
|
||||
unsigned format;
|
||||
unsigned stride;
|
||||
|
||||
bool online;
|
||||
bool key;
|
||||
|
||||
long double grab_ts;
|
||||
long double encode_begin_ts;
|
||||
long double encode_end_ts;
|
||||
} tmp_frame_s;
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
|
||||
char *obj;
|
||||
double lock_timeout;
|
||||
double wait_timeout;
|
||||
double drop_same_frames;
|
||||
|
||||
int fd;
|
||||
memsink_shared_s *mem;
|
||||
uint8_t *tmp_data;
|
||||
size_t tmp_data_allocated;
|
||||
uint64_t last_id;
|
||||
|
||||
tmp_frame_s *tmp_frame;
|
||||
} MemsinkObject;
|
||||
|
||||
|
||||
#define MEM(_next) self->mem->_next
|
||||
#define TMP(_next) self->tmp_frame->_next
|
||||
|
||||
|
||||
static void MemsinkObject_destroy_internals(MemsinkObject *self) {
|
||||
if (self->mem != NULL) {
|
||||
munmap(self->mem, sizeof(memsink_shared_s));
|
||||
@@ -41,10 +66,12 @@ static void MemsinkObject_destroy_internals(MemsinkObject *self) {
|
||||
close(self->fd);
|
||||
self->fd = -1;
|
||||
}
|
||||
if (self->tmp_data) {
|
||||
free(self->tmp_data);
|
||||
self->tmp_data = NULL;
|
||||
self->tmp_data_allocated = 0;
|
||||
if (self->tmp_frame) {
|
||||
if (TMP(data)) {
|
||||
free(TMP(data));
|
||||
}
|
||||
free(self->tmp_frame);
|
||||
self->tmp_frame = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,27 +79,29 @@ static int MemsinkObject_init(MemsinkObject *self, PyObject *args, PyObject *kwa
|
||||
self->lock_timeout = 1;
|
||||
self->wait_timeout = 1;
|
||||
|
||||
static char *kws[] = {"obj", "lock_timeout", "wait_timeout", NULL};
|
||||
static char *kws[] = {"obj", "lock_timeout", "wait_timeout", "drop_same_frames", NULL};
|
||||
if (!PyArg_ParseTupleAndKeywords(
|
||||
args, kwargs, "s|dd", kws,
|
||||
&self->obj, &self->lock_timeout, &self->wait_timeout)) {
|
||||
args, kwargs, "s|ddd", kws,
|
||||
&self->obj, &self->lock_timeout, &self->wait_timeout, &self->drop_same_frames)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
# define SET_TIMEOUT(_timeout) { \
|
||||
if (self->_timeout <= 0) { \
|
||||
PyErr_SetString(PyExc_ValueError, #_timeout " must be > 0"); \
|
||||
# define SET_DOUBLE(_field, _cond) { \
|
||||
if (!(self->_field _cond)) { \
|
||||
PyErr_SetString(PyExc_ValueError, #_field " must be " #_cond); \
|
||||
return -1; \
|
||||
} \
|
||||
}
|
||||
|
||||
SET_TIMEOUT(lock_timeout);
|
||||
SET_TIMEOUT(wait_timeout);
|
||||
SET_DOUBLE(lock_timeout, > 0);
|
||||
SET_DOUBLE(wait_timeout, > 0);
|
||||
SET_DOUBLE(drop_same_frames, >= 0);
|
||||
|
||||
# undef CHECK_TIMEOUT
|
||||
# undef SET_DOUBLE
|
||||
|
||||
self->tmp_data_allocated = 512 * 1024;
|
||||
A_REALLOC(self->tmp_data, self->tmp_data_allocated);
|
||||
A_CALLOC(self->tmp_frame, 1);
|
||||
TMP(allocated) = 512 * 1024;
|
||||
A_REALLOC(TMP(data), TMP(allocated));
|
||||
|
||||
if ((self->fd = shm_open(self->obj, O_RDWR, 0)) == -1) {
|
||||
PyErr_SetFromErrno(PyExc_OSError);
|
||||
@@ -128,92 +157,147 @@ static PyObject *MemsinkObject_exit(MemsinkObject *self, PyObject *Py_UNUSED(ign
|
||||
return PyObject_CallMethod((PyObject *)self, "close", "");
|
||||
}
|
||||
|
||||
static int wait_frame(MemsinkObject *self) {
|
||||
long double deadline_ts = get_now_monotonic() + self->wait_timeout;
|
||||
|
||||
# define RETURN_OS_ERROR { \
|
||||
Py_BLOCK_THREADS \
|
||||
PyErr_SetFromErrno(PyExc_OSError); \
|
||||
return -1; \
|
||||
}
|
||||
|
||||
long double now;
|
||||
do {
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
|
||||
int retval = flock_timedwait_monotonic(self->fd, self->lock_timeout);
|
||||
now = get_now_monotonic();
|
||||
|
||||
if (retval < 0 && errno != EWOULDBLOCK) {
|
||||
RETURN_OS_ERROR;
|
||||
|
||||
} else if (retval == 0) {
|
||||
if (MEM(magic) == MEMSINK_MAGIC && MEM(version) == MEMSINK_VERSION && TMP(id) != MEM(id)) {
|
||||
if (self->drop_same_frames > 0) {
|
||||
# define CMP(_field) (TMP(_field) == MEM(_field))
|
||||
if (
|
||||
CMP(used)
|
||||
&& CMP(width)
|
||||
&& CMP(height)
|
||||
&& CMP(format)
|
||||
&& CMP(stride)
|
||||
&& CMP(online)
|
||||
&& CMP(key)
|
||||
&& (TMP(ts) + self->drop_same_frames > now)
|
||||
&& !memcmp(TMP(data), MEM(data), MEM(used))
|
||||
) {
|
||||
TMP(id) = MEM(id);
|
||||
goto drop;
|
||||
}
|
||||
# undef CMP
|
||||
}
|
||||
|
||||
Py_BLOCK_THREADS
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (flock(self->fd, LOCK_UN) < 0) {
|
||||
RETURN_OS_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
drop:
|
||||
|
||||
if (usleep(1000) < 0) {
|
||||
RETURN_OS_ERROR;
|
||||
}
|
||||
|
||||
Py_END_ALLOW_THREADS
|
||||
|
||||
if (PyErr_CheckSignals() < 0) {
|
||||
return -1;
|
||||
}
|
||||
} while (now < deadline_ts);
|
||||
|
||||
# undef RETURN_OS_ERROR
|
||||
|
||||
return -2;
|
||||
}
|
||||
|
||||
static PyObject *MemsinkObject_wait_frame(MemsinkObject *self, PyObject *Py_UNUSED(ignored)) {
|
||||
if (self->mem == NULL || self->fd <= 0) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Closed");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
bool failed = false;
|
||||
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
long double deadline_ts = get_now_monotonic() + self->wait_timeout;
|
||||
do {
|
||||
int retval = flock_timedwait_monotonic(self->fd, self->lock_timeout);
|
||||
if (retval < 0 && errno != EWOULDBLOCK) {
|
||||
failed = true;
|
||||
break;
|
||||
} else if (retval == 0) {
|
||||
if (
|
||||
self->mem->magic == MEMSINK_MAGIC
|
||||
&& self->mem->version == MEMSINK_VERSION
|
||||
&& self->mem->id != self->last_id
|
||||
) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
if (flock(self->fd, LOCK_UN) < 0) {
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
errno = 0;
|
||||
usleep(1000);
|
||||
if (errno == EINTR) {
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
} while (get_now_monotonic() < deadline_ts);
|
||||
Py_END_ALLOW_THREADS
|
||||
|
||||
if (failed) {
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
}
|
||||
if (!found) {
|
||||
Py_RETURN_NONE;
|
||||
switch (wait_frame(self)) {
|
||||
case 0: break;
|
||||
case -2: Py_RETURN_NONE;
|
||||
default: return NULL;
|
||||
}
|
||||
|
||||
# define COPY(_type, _field) _type tmp_##_field = self->mem->_field
|
||||
COPY(unsigned, width);
|
||||
COPY(unsigned, height);
|
||||
COPY(unsigned, format);
|
||||
COPY(unsigned, stride);
|
||||
COPY(bool, online);
|
||||
COPY(double, grab_ts);
|
||||
COPY(double, encode_begin_ts);
|
||||
COPY(double, encode_end_ts);
|
||||
COPY(unsigned, used);
|
||||
# define COPY(_field) TMP(_field) = MEM(_field)
|
||||
COPY(width);
|
||||
COPY(height);
|
||||
COPY(format);
|
||||
COPY(stride);
|
||||
COPY(online);
|
||||
COPY(key);
|
||||
COPY(grab_ts);
|
||||
COPY(encode_begin_ts);
|
||||
COPY(encode_end_ts);
|
||||
COPY(used);
|
||||
# undef COPY
|
||||
|
||||
// Временный буффер используется для скорейшего разблокирования синка
|
||||
if (self->tmp_data_allocated < self->mem->used) {
|
||||
size_t size = self->mem->used + (512 * 1024);
|
||||
A_REALLOC(self->tmp_data, size);
|
||||
self->tmp_data_allocated = size;
|
||||
if (TMP(allocated) < MEM(used)) {
|
||||
size_t size = MEM(used) + (512 * 1024);
|
||||
A_REALLOC(TMP(data), size);
|
||||
TMP(allocated) = size;
|
||||
}
|
||||
memcpy(self->tmp_data, self->mem->data, self->mem->used);
|
||||
memcpy(TMP(data), MEM(data), MEM(used));
|
||||
TMP(used) = MEM(used);
|
||||
|
||||
self->mem->last_client_ts = get_now_monotonic();
|
||||
self->last_id = self->mem->id;
|
||||
TMP(id) = MEM(id);
|
||||
TMP(ts) = get_now_monotonic();
|
||||
MEM(last_client_ts) = TMP(ts);
|
||||
|
||||
if (flock(self->fd, LOCK_UN) < 0) {
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
}
|
||||
|
||||
PyObject *frame = PyDict_New();
|
||||
# define SET_VALUE(_field, _from, _to) PyDict_SetItemString(frame, #_field, Py##_to##_From##_from(tmp_##_field))
|
||||
SET_VALUE(width, Long, Long);
|
||||
SET_VALUE(height, Long, Long);
|
||||
SET_VALUE(format, Long, Long);
|
||||
SET_VALUE(stride, Long, Long);
|
||||
SET_VALUE(online, Long, Bool);
|
||||
SET_VALUE(grab_ts, Double, Float);
|
||||
SET_VALUE(encode_begin_ts, Double, Float);
|
||||
SET_VALUE(encode_end_ts, Double, Float);
|
||||
PyObject *dict_frame = PyDict_New();
|
||||
if (dict_frame == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
# define SET_VALUE(_key, _maker) { \
|
||||
PyObject *_tmp = _maker; \
|
||||
if (_tmp == NULL) { \
|
||||
return NULL; \
|
||||
} \
|
||||
if (PyDict_SetItemString(dict_frame, _key, _tmp) < 0) { \
|
||||
Py_DECREF(_tmp); \
|
||||
return NULL; \
|
||||
} \
|
||||
Py_DECREF(_tmp); \
|
||||
}
|
||||
# define SET_NUMBER(_key, _from, _to) SET_VALUE(#_key, Py##_to##_From##_from(TMP(_key)))
|
||||
|
||||
SET_NUMBER(width, Long, Long);
|
||||
SET_NUMBER(height, Long, Long);
|
||||
SET_NUMBER(format, Long, Long);
|
||||
SET_NUMBER(stride, Long, Long);
|
||||
SET_NUMBER(online, Long, Bool);
|
||||
SET_NUMBER(key, Long, Bool);
|
||||
SET_NUMBER(grab_ts, Double, Float);
|
||||
SET_NUMBER(encode_begin_ts, Double, Float);
|
||||
SET_NUMBER(encode_end_ts, Double, Float);
|
||||
SET_VALUE("data", PyBytes_FromStringAndSize((const char *)TMP(data), TMP(used)));
|
||||
|
||||
# undef SET_NUMBER
|
||||
# undef SET_VALUE
|
||||
PyDict_SetItemString(frame, "data", PyBytes_FromStringAndSize((const char *)self->tmp_data, tmp_used));
|
||||
return frame;
|
||||
|
||||
return dict_frame;
|
||||
}
|
||||
|
||||
static PyObject *MemsinkObject_is_opened(MemsinkObject *self, PyObject *Py_UNUSED(ignored)) {
|
||||
@@ -228,16 +312,18 @@ static PyObject *MemsinkObject_is_opened(MemsinkObject *self, PyObject *Py_UNUSE
|
||||
FIELD_GETTER(obj, String, Unicode)
|
||||
FIELD_GETTER(lock_timeout, Double, Float)
|
||||
FIELD_GETTER(wait_timeout, Double, Float)
|
||||
FIELD_GETTER(drop_same_frames, Double, Float)
|
||||
|
||||
#undef FIELD_GETTER
|
||||
|
||||
static PyMethodDef MemsinkObject_methods[] = {
|
||||
# define ADD_METHOD(_meth, _flags) {.ml_name = #_meth, .ml_meth = (PyCFunction)MemsinkObject_##_meth, .ml_flags = (_flags)}
|
||||
ADD_METHOD(close, METH_NOARGS),
|
||||
ADD_METHOD(enter, METH_NOARGS),
|
||||
ADD_METHOD(exit, METH_VARARGS),
|
||||
ADD_METHOD(wait_frame, METH_NOARGS),
|
||||
ADD_METHOD(is_opened, METH_NOARGS),
|
||||
# define ADD_METHOD(_name, _method, _flags) \
|
||||
{.ml_name = _name, .ml_meth = (PyCFunction)MemsinkObject_##_method, .ml_flags = (_flags)}
|
||||
ADD_METHOD("close", close, METH_NOARGS),
|
||||
ADD_METHOD("__enter__", enter, METH_NOARGS),
|
||||
ADD_METHOD("__exit__", exit, METH_VARARGS),
|
||||
ADD_METHOD("wait_frame", wait_frame, METH_NOARGS),
|
||||
ADD_METHOD("is_opened", is_opened, METH_NOARGS),
|
||||
{},
|
||||
# undef ADD_METHOD
|
||||
};
|
||||
@@ -247,6 +333,7 @@ static PyGetSetDef MemsinkObject_getsets[] = {
|
||||
ADD_GETTER(obj),
|
||||
ADD_GETTER(lock_timeout),
|
||||
ADD_GETTER(wait_timeout),
|
||||
ADD_GETTER(drop_same_frames),
|
||||
{},
|
||||
# undef ADD_GETTER
|
||||
};
|
||||
@@ -288,3 +375,6 @@ PyMODINIT_FUNC PyInit_ustreamer(void) { // cppcheck-suppress unusedFunction
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
#undef TMP
|
||||
#undef MEM
|
||||
@@ -213,6 +213,8 @@ static int _dump_sink(const char *sink_name, unsigned sink_timeout, _output_cont
|
||||
unsigned fps_accum = 0;
|
||||
long long fps_second = 0;
|
||||
|
||||
long double last_ts = 0;
|
||||
|
||||
while (!global_stop) {
|
||||
int error = memsink_client_get(sink, frame);
|
||||
if (error == 0) {
|
||||
@@ -220,11 +222,12 @@ static int _dump_sink(const char *sink_name, unsigned sink_timeout, _output_cont
|
||||
const long long now_second = floor_ms(now);
|
||||
|
||||
char fourcc_str[8];
|
||||
LOG_VERBOSE("Frame: size=%zu, resolution=%ux%u, fourcc=%s, stride=%u, online=%d, latency=%.3Lf",
|
||||
LOG_VERBOSE("Frame: size=%zu, res=%ux%u, fourcc=%s, stride=%u, online=%d, key=%d, latency=%.3Lf, diff=%.3Lf",
|
||||
frame->used, frame->width, frame->height,
|
||||
fourcc_to_string(frame->format, fourcc_str, 8),
|
||||
frame->stride, frame->online,
|
||||
now - frame->grab_ts);
|
||||
frame->stride, frame->online, frame->key,
|
||||
now - frame->grab_ts, (last_ts ? now - last_ts : 0));
|
||||
last_ts = now;
|
||||
|
||||
LOG_DEBUG(" grab_ts=%.3Lf, encode_begin_ts=%.3Lf, encode_end_ts=%.3Lf",
|
||||
frame->grab_ts, frame->encode_begin_ts, frame->encode_end_ts);
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef VERSION
|
||||
# define VERSION "3.6"
|
||||
# define VERSION "3.21"
|
||||
#endif
|
||||
|
||||
@@ -81,6 +81,7 @@ void frame_copy_meta(const frame_s *src, frame_s *dest) {
|
||||
COPY(format);
|
||||
COPY(stride);
|
||||
COPY(online);
|
||||
COPY(key);
|
||||
COPY(grab_ts);
|
||||
COPY(encode_begin_ts);
|
||||
COPY(encode_end_ts);
|
||||
@@ -98,6 +99,7 @@ bool frame_compare(const frame_s *a, const frame_s *b) {
|
||||
&& CMP(format)
|
||||
&& CMP(stride)
|
||||
&& CMP(online)
|
||||
&& CMP(key)
|
||||
&& !memcmp(a->data, b->data, b->used)
|
||||
);
|
||||
# undef CMP
|
||||
|
||||
@@ -51,6 +51,7 @@ typedef struct {
|
||||
// https://medium.com/@oleg.shipitko/what-does-stride-mean-in-image-processing-bba158a72bcd
|
||||
|
||||
bool online;
|
||||
bool key;
|
||||
|
||||
long double grab_ts;
|
||||
long double encode_begin_ts;
|
||||
|
||||
@@ -40,7 +40,11 @@ memsink_s *memsink_init(
|
||||
|
||||
LOG_INFO("Using %s-sink: %s", name, obj);
|
||||
|
||||
if ((sink->fd = shm_open(sink->obj, (server ? O_RDWR | O_CREAT : O_RDWR), mode)) == -1) {
|
||||
mode_t mask = umask(0);
|
||||
sink->fd = shm_open(sink->obj, (server ? O_RDWR | O_CREAT : O_RDWR), mode);
|
||||
umask(mask);
|
||||
if (sink->fd == -1) {
|
||||
umask(mask);
|
||||
LOG_PERROR("%s-sink: Can't open shared memory", name);
|
||||
goto error;
|
||||
}
|
||||
@@ -105,7 +109,7 @@ bool memsink_server_check(memsink_s *sink, const frame_s *frame) {
|
||||
sink->has_clients = (sink->mem->last_client_ts + sink->client_ttl > get_now_monotonic());
|
||||
|
||||
# define NEQ(_field) (sink->mem->_field != frame->_field)
|
||||
bool retval = (sink->has_clients || NEQ(width) || NEQ(height) || NEQ(format) || NEQ(stride) || NEQ(online));
|
||||
bool retval = (sink->has_clients || NEQ(width) || NEQ(height) || NEQ(format) || NEQ(stride) || NEQ(online) || NEQ(key));
|
||||
# undef NEQ
|
||||
|
||||
if (flock(sink->fd, LOCK_UN) < 0) {
|
||||
@@ -138,6 +142,7 @@ int memsink_server_put(memsink_s *sink, const frame_s *frame) {
|
||||
COPY(format);
|
||||
COPY(stride);
|
||||
COPY(online);
|
||||
COPY(key);
|
||||
COPY(grab_ts);
|
||||
COPY(encode_begin_ts);
|
||||
COPY(encode_end_ts);
|
||||
@@ -191,6 +196,7 @@ int memsink_client_get(memsink_s *sink, frame_s *frame) { // cppcheck-suppress u
|
||||
COPY(format);
|
||||
COPY(stride);
|
||||
COPY(online);
|
||||
COPY(key);
|
||||
COPY(grab_ts);
|
||||
COPY(encode_begin_ts);
|
||||
COPY(encode_end_ts);
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
|
||||
#define MEMSINK_MAGIC ((uint64_t)0xCAFEBABECAFEBABE)
|
||||
#define MEMSINK_VERSION ((uint32_t)1)
|
||||
#define MEMSINK_VERSION ((uint32_t)2)
|
||||
|
||||
#ifndef CFG_MEMSINK_MAX_DATA
|
||||
# define CFG_MEMSINK_MAX_DATA 33554432
|
||||
@@ -49,6 +49,7 @@ typedef struct {
|
||||
unsigned format;
|
||||
unsigned stride;
|
||||
bool online;
|
||||
bool key;
|
||||
|
||||
long double grab_ts;
|
||||
long double encode_begin_ts;
|
||||
|
||||
@@ -135,7 +135,9 @@ INLINE int flock_timedwait_monotonic(int fd, long double timeout) {
|
||||
if (retval == 0 || errno != EWOULDBLOCK || get_now_monotonic() > deadline_ts) {
|
||||
break;
|
||||
}
|
||||
usleep(1000);
|
||||
if (usleep(1000) < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ static const struct {
|
||||
{"UYVY", V4L2_PIX_FMT_UYVY},
|
||||
{"RGB565", V4L2_PIX_FMT_RGB565},
|
||||
{"RGB24", V4L2_PIX_FMT_RGB24},
|
||||
{"JPEG", V4L2_PIX_FMT_MJPEG},
|
||||
{"MJPEG", V4L2_PIX_FMT_MJPEG},
|
||||
{"JPEG", V4L2_PIX_FMT_JPEG},
|
||||
};
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
#define STANDARDS_STR "PAL, NTSC, SECAM"
|
||||
|
||||
#define FORMAT_UNKNOWN -1
|
||||
#define FORMATS_STR "YUYV, UYVY, RGB565, RGB24, JPEG"
|
||||
#define FORMATS_STR "YUYV, UYVY, RGB565, RGB24, MJPEG, JPEG"
|
||||
|
||||
#define IO_METHOD_UNKNOWN -1
|
||||
#define IO_METHODS_STR "MMAP, USERPTR"
|
||||
|
||||
@@ -27,7 +27,6 @@ static const OMX_U32 _INPUT_PORT = 340;
|
||||
static const OMX_U32 _OUTPUT_PORT = 341;
|
||||
|
||||
|
||||
static int _vcos_semwait(VCOS_SEMAPHORE_T *sem);
|
||||
static int _omx_init_component(omx_encoder_s *omx);
|
||||
static int _omx_init_disable_ports(omx_encoder_s *omx);
|
||||
static int _omx_setup_input(omx_encoder_s *omx, const frame_s *frame);
|
||||
@@ -194,7 +193,7 @@ int omx_encoder_compress(omx_encoder_s *omx, const frame_s *src, frame_s *dest)
|
||||
}
|
||||
}
|
||||
|
||||
if (_vcos_semwait(&omx->handler_sem) != 0) {
|
||||
if (vcos_my_semwait("", &omx->handler_sem, 1) < 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -204,40 +203,6 @@ int omx_encoder_compress(omx_encoder_s *omx, const frame_s *src, frame_s *dest)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int _vcos_semwait(VCOS_SEMAPHORE_T *sem) {
|
||||
// vcos_semaphore_wait() can wait infinite
|
||||
// vcos_semaphore_wait_timeout() is broken by design:
|
||||
// - https://github.com/pikvm/ustreamer/issues/56
|
||||
// - https://github.com/raspberrypi/userland/issues/658
|
||||
// CFG_OMX_SEMWAIT_TIMEOUT is ugly busyloop
|
||||
// Три стула.
|
||||
|
||||
# ifdef CFG_OMX_SEMWAIT_TIMEOUT
|
||||
long double deadline_ts = get_now_monotonic() + (long double)CFG_OMX_SEMWAIT_TIMEOUT; // Seconds
|
||||
VCOS_STATUS_T sem_status;
|
||||
|
||||
while (true) {
|
||||
sem_status = vcos_semaphore_trywait(sem);
|
||||
if (sem_status == VCOS_SUCCESS) {
|
||||
return 0;
|
||||
} else if (sem_status != VCOS_EAGAIN || get_now_monotonic() > deadline_ts) {
|
||||
goto error;
|
||||
}
|
||||
usleep(1000);
|
||||
}
|
||||
|
||||
error:
|
||||
switch (sem_status) {
|
||||
case VCOS_EAGAIN: LOG_ERROR("Can't wait VCOS semaphore: EAGAIN (timeout)"); break;
|
||||
case VCOS_EINVAL: LOG_ERROR("Can't wait VCOS semaphore: EINVAL"); break;
|
||||
default: LOG_ERROR("Can't wait VCOS semaphore: %d", sem_status); break;
|
||||
}
|
||||
return -1;
|
||||
# else
|
||||
return (vcos_semaphore_wait(sem) == VCOS_SUCCESS ? 0 : -1);
|
||||
# endif
|
||||
}
|
||||
|
||||
static int _omx_init_component(omx_encoder_s *omx) {
|
||||
OMX_ERRORTYPE error;
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#include "../../../libs/logging.h"
|
||||
#include "../../../libs/frame.h"
|
||||
|
||||
#include "vcos.h"
|
||||
#include "formatters.h"
|
||||
#include "component.h"
|
||||
|
||||
|
||||
55
src/ustreamer/encoders/omx/vcos.c
Normal file
55
src/ustreamer/encoders/omx/vcos.c
Normal file
@@ -0,0 +1,55 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2021 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
#include "vcos.h"
|
||||
|
||||
|
||||
int vcos_my_semwait(const char *prefix, VCOS_SEMAPHORE_T *sem, long double timeout) {
|
||||
// vcos_semaphore_wait() can wait infinite
|
||||
// vcos_semaphore_wait_timeout() is broken by design:
|
||||
// - https://github.com/pikvm/ustreamer/issues/56
|
||||
// - https://github.com/raspberrypi/userland/issues/658
|
||||
// - The current approach is an ugly busyloop
|
||||
// Три стула.
|
||||
|
||||
long double deadline_ts = get_now_monotonic() + timeout;
|
||||
VCOS_STATUS_T sem_status;
|
||||
|
||||
while (true) {
|
||||
sem_status = vcos_semaphore_trywait(sem);
|
||||
if (sem_status == VCOS_SUCCESS) {
|
||||
return 0;
|
||||
} else if (sem_status != VCOS_EAGAIN || get_now_monotonic() > deadline_ts) {
|
||||
break;
|
||||
}
|
||||
if (usleep(1000) < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (sem_status) {
|
||||
case VCOS_EAGAIN: LOG_ERROR("%sCan't wait VCOS semaphore: EAGAIN (timeout)", prefix); break;
|
||||
case VCOS_EINVAL: LOG_ERROR("%sCan't wait VCOS semaphore: EINVAL", prefix); break;
|
||||
default: LOG_ERROR("%sCan't wait VCOS semaphore: %d", prefix, sem_status); break;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
31
src/ustreamer/encoders/omx/vcos.h
Normal file
31
src/ustreamer/encoders/omx/vcos.h
Normal file
@@ -0,0 +1,31 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2021 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <interface/vcos/vcos_semaphore.h>
|
||||
|
||||
#include "../../../libs/tools.h"
|
||||
#include "../../../libs/logging.h"
|
||||
|
||||
|
||||
int vcos_my_semwait(const char *prefix, VCOS_SEMAPHORE_T *sem, long double timeout);
|
||||
@@ -212,7 +212,8 @@ int h264_encoder_prepare(h264_encoder_s *enc, const frame_s *frame, bool zero_co
|
||||
SET_PORT_PARAM(output, uint32, VIDEO_ENCODE_PEAK_RATE, enc->bitrate * 1000);
|
||||
SET_PORT_PARAM(output, uint32, VIDEO_ENCODE_MIN_QUANT, 16);
|
||||
SET_PORT_PARAM(output, uint32, VIDEO_ENCODE_MAX_QUANT, 34);
|
||||
SET_PORT_PARAM(output, uint32, VIDEO_ENCODE_FRAME_LIMIT_BITS, 1000000);
|
||||
// Этот параметр с этим значением фризит кодирование изображения из черно-белой консоли
|
||||
// SET_PORT_PARAM(output, uint32, VIDEO_ENCODE_FRAME_LIMIT_BITS, 1000000);
|
||||
SET_PORT_PARAM(output, uint32, VIDEO_ENCODE_H264_AU_DELIMITERS, MMAL_FALSE);
|
||||
}
|
||||
|
||||
@@ -224,7 +225,7 @@ int h264_encoder_prepare(h264_encoder_s *enc, const frame_s *frame, bool zero_co
|
||||
|
||||
error:
|
||||
_h264_encoder_cleanup(enc);
|
||||
LOG_ERROR("H264: Encoder disabled due error (prepare)");
|
||||
LOG_ERROR("H264: Encoder destroyed due an error (prepare)");
|
||||
return -1;
|
||||
|
||||
# undef ENABLE_PORT
|
||||
@@ -275,7 +276,7 @@ int h264_encoder_compress(h264_encoder_s *enc, const frame_s *src, int src_vcsm_
|
||||
|
||||
if (_h264_encoder_compress_raw(enc, src, src_vcsm_handle, dest, force_key) < 0) {
|
||||
_h264_encoder_cleanup(enc);
|
||||
LOG_ERROR("H264: Encoder disabled due error (compress)");
|
||||
LOG_ERROR("H264: Encoder destroyed due an error (compress)");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -339,7 +340,9 @@ static int _h264_encoder_compress_raw(h264_encoder_s *enc, const frame_s *src, i
|
||||
|
||||
error = mmal_wrapper_buffer_get_full(enc->output_port, &out, 0);
|
||||
if (error == MMAL_EAGAIN) {
|
||||
vcos_semaphore_wait(&enc->handler_sem);
|
||||
if (vcos_my_semwait("H264: ", &enc->handler_sem, 1) < 0) {
|
||||
return -1;
|
||||
}
|
||||
continue;
|
||||
} else if (error != MMAL_SUCCESS) {
|
||||
LOG_ERROR_MMAL(error, "H264: Can't get MMAL output buffer");
|
||||
@@ -347,6 +350,7 @@ static int _h264_encoder_compress_raw(h264_encoder_s *enc, const frame_s *src, i
|
||||
}
|
||||
|
||||
frame_append_data(dest, out->data, out->length);
|
||||
dest->key = out->flags & MMAL_BUFFER_HEADER_FLAG_KEYFRAME;
|
||||
|
||||
eos = out->flags & MMAL_BUFFER_HEADER_FLAG_EOS;
|
||||
mmal_buffer_header_release(out);
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include "../../libs/tools.h"
|
||||
#include "../../libs/logging.h"
|
||||
#include "../../libs/frame.h"
|
||||
#include "../encoders/omx/vcos.h"
|
||||
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -39,6 +39,8 @@ static void _http_queue_send_stream(server_s *server, bool stream_updated, bool
|
||||
|
||||
static bool _expose_new_frame(server_s *server);
|
||||
|
||||
static char *_http_get_client_hostport(struct evhttp_request *request);
|
||||
|
||||
|
||||
#define RUN(_next) server->run->_next
|
||||
#define STREAM(_next) RUN(stream->_next)
|
||||
@@ -94,6 +96,7 @@ void server_destroy(server_s *server) {
|
||||
for (stream_client_s *client = RUN(stream_clients); client != NULL;) {
|
||||
stream_client_s *next = client->next;
|
||||
free(client->key);
|
||||
free(client->hostport);
|
||||
free(client);
|
||||
client = next;
|
||||
}
|
||||
@@ -368,7 +371,7 @@ static void _http_callback_state(struct evhttp_request *request, void *v_server)
|
||||
|
||||
for (stream_client_s * client = RUN(stream_clients); client != NULL; client = client->next) {
|
||||
assert(evbuffer_add_printf(buf,
|
||||
"\"%s\": {\"fps\": %u, \"extra_headers\": %s, \"advance_headers\": %s,"
|
||||
"\"%" PRIx64 "\": {\"fps\": %u, \"extra_headers\": %s, \"advance_headers\": %s,"
|
||||
" \"dual_final_frames\": %s, \"zero_data\": %s}%s",
|
||||
client->id,
|
||||
client->fps,
|
||||
@@ -473,9 +476,8 @@ static void _http_callback_stream(struct evhttp_request *request, void *v_server
|
||||
# undef PARSE_PARAM
|
||||
evhttp_clear_headers(¶ms);
|
||||
|
||||
uuid_t uuid;
|
||||
uuid_generate(uuid);
|
||||
uuid_unparse_lower(uuid, client->id);
|
||||
client->hostport = _http_get_client_hostport(request);
|
||||
client->id = get_now_id();
|
||||
|
||||
if (RUN(stream_clients) == NULL) {
|
||||
RUN(stream_clients) = client;
|
||||
@@ -495,23 +497,18 @@ static void _http_callback_stream(struct evhttp_request *request, void *v_server
|
||||
# endif
|
||||
}
|
||||
|
||||
char *client_addr;
|
||||
unsigned short client_port;
|
||||
evhttp_connection_get_peer(conn, &client_addr, &client_port);
|
||||
|
||||
LOG_INFO("HTTP: Registered client: [%s]:%u, id=%s; clients now: %u",
|
||||
client_addr, client_port, client->id, RUN(stream_clients_count));
|
||||
|
||||
LOG_INFO("HTTP: Registered client: %s, id=%" PRIx64 "; clients now: %u",
|
||||
client->hostport, client->id, RUN(stream_clients_count));
|
||||
|
||||
struct bufferevent *buf_event = evhttp_connection_get_bufferevent(conn);
|
||||
if (server->tcp_nodelay && !RUN(unix_fd)) {
|
||||
evutil_socket_t fd;
|
||||
int on = 1;
|
||||
|
||||
LOG_DEBUG("HTTP: Setting up TCP_NODELAY to the client [%s]:%u ...", client_addr, client_port);
|
||||
LOG_DEBUG("HTTP: Setting up TCP_NODELAY to the client %s ...", client->hostport);
|
||||
assert((fd = bufferevent_getfd(buf_event)) >= 0);
|
||||
if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (void *)&on, sizeof(on)) != 0) {
|
||||
LOG_PERROR("HTTP: Can't set TCP_NODELAY to the client [%s]:%u", client_addr, client_port);
|
||||
LOG_PERROR("HTTP: Can't set TCP_NODELAY to the client %s", client->hostport);
|
||||
}
|
||||
}
|
||||
bufferevent_setcb(buf_event, NULL, NULL, _http_callback_stream_error, (void *)client);
|
||||
@@ -575,7 +572,7 @@ static void _http_callback_stream_write(struct bufferevent *buf_event, void *v_c
|
||||
"Cache-Control: no-store, no-cache, must-revalidate, proxy-revalidate, pre-check=0, post-check=0, max-age=0" RN
|
||||
"Pragma: no-cache" RN
|
||||
"Expires: Mon, 3 Jan 2000 12:34:56 GMT" RN
|
||||
"Set-Cookie: stream_client=%s/%s; path=/; max-age=30" RN
|
||||
"Set-Cookie: stream_client=%s/%" PRIx64 "; path=/; max-age=30" RN
|
||||
"Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY RN
|
||||
RN
|
||||
"--" BOUNDARY RN,
|
||||
@@ -670,17 +667,10 @@ static void _http_callback_stream_error(UNUSED struct bufferevent *buf_event, UN
|
||||
# endif
|
||||
}
|
||||
|
||||
char *client_addr = "???";
|
||||
unsigned short client_port = 0;
|
||||
LOG_INFO("HTTP: Disconnected client: %s, id=%" PRIx64 ", %s; clients now: %u",
|
||||
client->hostport, client->id, reason, RUN(stream_clients_count));
|
||||
|
||||
struct evhttp_connection *conn = evhttp_request_get_connection(client->request);
|
||||
if (conn) {
|
||||
evhttp_connection_get_peer(conn, &client_addr, &client_port);
|
||||
}
|
||||
|
||||
LOG_INFO("HTTP: Disconnected client: [%s]:%u, id=%s, %s; clients now: %u",
|
||||
client_addr, client_port, client->id, reason, RUN(stream_clients_count));
|
||||
|
||||
if (conn) {
|
||||
evhttp_connection_free(conn);
|
||||
}
|
||||
@@ -694,6 +684,7 @@ static void _http_callback_stream_error(UNUSED struct bufferevent *buf_event, UN
|
||||
client->next->prev = client->prev;
|
||||
}
|
||||
free(client->key);
|
||||
free(client->hostport);
|
||||
free(client);
|
||||
|
||||
free(reason);
|
||||
@@ -836,3 +827,40 @@ static bool _expose_new_frame(server_s *server) {
|
||||
#undef VID
|
||||
#undef STREAM
|
||||
#undef RUN
|
||||
|
||||
static char *_http_get_client_hostport(struct evhttp_request *request) {
|
||||
char *addr = NULL;
|
||||
unsigned short port = 0;
|
||||
struct evhttp_connection *conn = evhttp_request_get_connection(request);
|
||||
if (conn) {
|
||||
char *peer;
|
||||
evhttp_connection_get_peer(conn, &peer, &port);
|
||||
assert(addr = strdup(peer));
|
||||
}
|
||||
|
||||
const char *xff = evhttp_find_header(evhttp_request_get_input_headers(request), "X-Forwarded-For");
|
||||
if (xff) {
|
||||
if (addr) {
|
||||
free(addr);
|
||||
}
|
||||
assert(addr = strndup(xff, 1024));
|
||||
for (unsigned index = 0; addr[index]; ++index) {
|
||||
if (addr[index] == ',') {
|
||||
addr[index] = '\0';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (addr == NULL) {
|
||||
assert(addr = strdup("???"));
|
||||
}
|
||||
|
||||
char *hostport;
|
||||
size_t hostport_len = strlen(addr) + 64;
|
||||
A_CALLOC(hostport, hostport_len);
|
||||
|
||||
snprintf(hostport, hostport_len, "[%s]:%u", addr, port);
|
||||
free(addr);
|
||||
return hostport;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <stdbool.h>
|
||||
#include <stdatomic.h>
|
||||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <assert.h>
|
||||
@@ -47,8 +48,6 @@
|
||||
#include <event2/bufferevent.h>
|
||||
#include <event2/keyvalq_struct.h>
|
||||
|
||||
#include <uuid/uuid.h>
|
||||
|
||||
#ifndef EVTHREAD_USE_PTHREADS_IMPLEMENTED
|
||||
# error Required libevent-pthreads support
|
||||
#endif
|
||||
@@ -83,7 +82,8 @@ typedef struct stream_client_sx {
|
||||
bool dual_final_frames;
|
||||
bool zero_data;
|
||||
|
||||
char id[37]; // ex. "1b4e28ba-2fa1-11d2-883f-0016d3cca427" + "\0"
|
||||
char *hostport;
|
||||
uint64_t id;
|
||||
bool need_initial;
|
||||
bool need_first_frame;
|
||||
bool updated_prev;
|
||||
|
||||
@@ -617,7 +617,8 @@ static void _help(FILE *fp, device_s *dev, encoder_s *enc, stream_s *stream, ser
|
||||
SAY(" -K|--last-as-blank <sec> ──────────── Show the last frame received from the camera after it was disconnected,");
|
||||
SAY(" but no more than specified time (or endlessly if 0 is specified).");
|
||||
SAY(" If the device has not yet been online, display 'NO SIGNAL' or the image");
|
||||
SAY(" specified by option --blank. Default: disabled.\n");
|
||||
SAY(" specified by option --blank. Default: disabled.");
|
||||
SAY(" Note: currently this option has no effect on memory sinks.\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(" --device-timeout <sec> ────────────── Timeout for device querying. Default: %u.\n", dev->timeout);
|
||||
|
||||
@@ -25,11 +25,25 @@
|
||||
|
||||
static workers_pool_s *_stream_init_loop(stream_s *stream);
|
||||
static workers_pool_s *_stream_init_one(stream_s *stream);
|
||||
static bool _stream_expose_frame(stream_s *stream, frame_s *frame, unsigned captured_fps);
|
||||
static void _stream_expose_frame(stream_s *stream, frame_s *frame, unsigned captured_fps);
|
||||
|
||||
|
||||
#define RUN(_next) stream->run->_next
|
||||
|
||||
#define SINK_PUT(_sink, _frame) { \
|
||||
if (stream->_sink && memsink_server_check(stream->_sink, _frame)) {\
|
||||
memsink_server_put(stream->_sink, _frame); \
|
||||
} \
|
||||
}
|
||||
|
||||
#ifdef WITH_OMX
|
||||
# define H264_PUT(_frame, _vcsm_handle, _force_key) { \
|
||||
if (RUN(h264)) { \
|
||||
h264_stream_process(RUN(h264), _frame, _vcsm_handle, _force_key); \
|
||||
} \
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
stream_s *stream_init(device_s *dev, encoder_s *enc) {
|
||||
stream_runtime_s *run;
|
||||
@@ -194,16 +208,9 @@ void stream_loop(stream_s *stream) {
|
||||
workers_pool_assign(pool, ready_wr);
|
||||
LOG_DEBUG("Assigned new frame in buffer %d to worker %s", buf_index, ready_wr->name);
|
||||
|
||||
if (stream->raw_sink) {
|
||||
if (memsink_server_check(stream->raw_sink, &hw->raw)) {
|
||||
memsink_server_put(stream->raw_sink, &hw->raw);
|
||||
}
|
||||
}
|
||||
|
||||
SINK_PUT(raw_sink, &hw->raw);
|
||||
# ifdef WITH_OMX
|
||||
if (RUN(h264)) {
|
||||
h264_stream_process(RUN(h264), &hw->raw, hw->vcsm_handle, h264_force_key);
|
||||
}
|
||||
H264_PUT(&hw->raw, hw->vcsm_handle, h264_force_key);
|
||||
# endif
|
||||
}
|
||||
} else if (buf_index != -2) { // -2 for broken frame
|
||||
@@ -253,18 +260,7 @@ static workers_pool_s *_stream_init_loop(stream_s *stream) {
|
||||
LOG_DEBUG("%s: stream->run->stop=%d", __FUNCTION__, atomic_load(&RUN(stop)));
|
||||
|
||||
while (!atomic_load(&RUN(stop))) {
|
||||
if (_stream_expose_frame(stream, NULL, 0)) {
|
||||
if (stream->raw_sink) {
|
||||
if (memsink_server_check(stream->raw_sink, stream->blank)) {
|
||||
memsink_server_put(stream->raw_sink, stream->blank);
|
||||
}
|
||||
}
|
||||
# ifdef WITH_OMX
|
||||
if (RUN(h264)) {
|
||||
h264_stream_process(RUN(h264), stream->blank, -1, false);
|
||||
}
|
||||
# endif
|
||||
}
|
||||
_stream_expose_frame(stream, NULL, 0);
|
||||
|
||||
if (access(stream->dev->path, R_OK|W_OK) < 0) {
|
||||
if (access_error != errno) {
|
||||
@@ -308,11 +304,10 @@ static workers_pool_s *_stream_init_one(stream_s *stream) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool _stream_expose_frame(stream_s *stream, frame_s *frame, unsigned captured_fps) {
|
||||
static void _stream_expose_frame(stream_s *stream, frame_s *frame, unsigned captured_fps) {
|
||||
# define VID(_next) RUN(video->_next)
|
||||
|
||||
frame_s *new = NULL;
|
||||
bool changed = false;
|
||||
|
||||
A_MUTEX_LOCK(&VID(mutex));
|
||||
|
||||
@@ -322,7 +317,11 @@ static bool _stream_expose_frame(stream_s *stream, frame_s *frame, unsigned capt
|
||||
LOG_DEBUG("Exposed ALIVE video frame");
|
||||
|
||||
} else {
|
||||
if (VID(frame->online)) { // Если переходим из online в offline
|
||||
if (VID(frame->used == 0)) {
|
||||
new = stream->blank; // Инициализация
|
||||
RUN(last_as_blank_ts) = 0;
|
||||
|
||||
} else if (VID(frame->online)) { // Если переходим из online в offline
|
||||
if (stream->last_as_blank < 0) { // Если last_as_blank выключен, просто покажем старую картинку
|
||||
new = stream->blank;
|
||||
LOG_INFO("Changed video frame to BLANK");
|
||||
@@ -332,6 +331,7 @@ static bool _stream_expose_frame(stream_s *stream, frame_s *frame, unsigned capt
|
||||
} else { // last_as_blank == 0 - показываем последний фрейм вечно
|
||||
LOG_INFO("Freezed last ALIVE video frame forever");
|
||||
}
|
||||
|
||||
} else if (stream->last_as_blank < 0) {
|
||||
new = stream->blank;
|
||||
// LOG_INFO("Changed video frame to BLANK");
|
||||
@@ -350,26 +350,28 @@ static bool _stream_expose_frame(stream_s *stream, frame_s *frame, unsigned capt
|
||||
|
||||
if (new) {
|
||||
frame_copy(new, VID(frame));
|
||||
changed = true;
|
||||
} else if (VID(frame->used) == 0) { // Инициализация
|
||||
frame_copy(stream->blank, VID(frame));
|
||||
frame = NULL;
|
||||
changed = true;
|
||||
}
|
||||
VID(frame->online) = frame;
|
||||
VID(frame->online) = (bool)frame;
|
||||
VID(captured_fps) = captured_fps;
|
||||
atomic_store(&VID(updated), true);
|
||||
|
||||
A_MUTEX_UNLOCK(&VID(mutex));
|
||||
|
||||
if (changed && stream->sink) {
|
||||
if (memsink_server_check(stream->sink, VID(frame))) {
|
||||
memsink_server_put(stream->sink, VID(frame));
|
||||
}
|
||||
}
|
||||
new = (frame ? frame : stream->blank);
|
||||
SINK_PUT(sink, new);
|
||||
|
||||
return changed;
|
||||
if (frame == NULL) {
|
||||
SINK_PUT(raw_sink, stream->blank);
|
||||
# ifdef WITH_OMX
|
||||
H264_PUT(stream->blank, -1, false);
|
||||
# endif
|
||||
}
|
||||
|
||||
# undef VID
|
||||
}
|
||||
|
||||
#ifdef WITH_OMX
|
||||
# undef H264_PUT
|
||||
#endif
|
||||
#undef SINK_PUT
|
||||
#undef RUN
|
||||
|
||||
Reference in New Issue
Block a user