Compare commits

..

156 Commits
v3.5 ... v4.11

Author SHA1 Message Date
Maxim Devaev
496c9300fc Bump version: 4.10 → 4.11 2021-12-31 01:28:51 +03:00
Maxim Devaev
5b7780cf7c using python-3.10 2021-12-31 01:26:47 +03:00
Maxim Devaev
421f4a1f2e Bump version: 4.9 → 4.10 2021-11-28 08:43:41 +03:00
Maxim Devaev
55a5d4bbdd real bitrate range 2021-11-28 08:40:56 +03:00
Maxim Devaev
666ae0c4f1 Merge pull request #133 from jtrmal/patch-1
correct/updated package name is libgpiod-dev
2021-11-26 03:33:04 +03:00
Jan "yenda" Trmal
ca3afc074c correct/updated package name is libgpiod-dev 2021-11-21 12:19:03 -05:00
Maxim Devaev
030077fb47 Bump version: 4.8 → 4.9 2021-10-30 11:40:31 +03:00
Maxim Devaev
61ddff8b6e Merge pull request #129 from b-rad15/master
Change html links to relative not absolute
2021-10-30 11:27:20 +03:00
Bradley O'Connell
27fbfe149e changed html links to relative not absolute 2021-10-30 03:20:57 -04:00
Maxim Devaev
349f655cd9 Bump version: 4.7 → 4.8 2021-10-19 15:56:19 +03:00
Maxim Devaev
f5c0a15967 removed ansible link 2021-10-19 15:55:34 +03:00
Maxim Devaev
d822ae0890 Fixed #121: static inline 2021-10-19 15:47:42 +03:00
Maxim Devaev
2d82adb2ba Bump version: 4.6 → 4.7 2021-10-17 00:14:25 +03:00
Maxim Devaev
56b21274d1 ru doc, etc 2021-10-17 00:13:01 +03:00
Maxim Devaev
76329ba5a6 Fixed #128: added mjpg_streamer compatibility for --static 2021-10-17 00:08:13 +03:00
Maxim Devaev
4d5c5fffb4 Merge pull request #127 from tiny-pilot/nginx-doc
Document how to integrate uStreamer with nginx
2021-10-17 00:07:55 +03:00
Michael Lynch
29234c330b Document how to integrate uStreamer with nginx
This change documents a gotcha that can occur when clients deploy uStreamer behind an nginx proxy. By default, nginx buffers responses, which introduces latency into the video stream. Disabling the buffer eliminates this latency.
2021-10-14 10:02:42 -04:00
Maxim Devaev
690d291818 Bump version: 4.5 → 4.6 2021-09-27 23:14:24 +03:00
Maxim Devaev
8cfb3fc301 lint fix 2021-09-27 22:55:01 +03:00
Maxim Devaev
69ab3fa831 ustreamer-dump: count and interval 2021-09-27 22:54:08 +03:00
Maxim Devaev
79d6c76084 Bump version: 4.4 → 4.5 2021-08-13 11:02:34 +03:00
Maxim Devaev
f15d2fc194 fixed .dockerignore 2021-08-13 10:49:23 +03:00
Maxim Devaev
99aa4828c5 fixed .gitignore 2021-08-13 10:48:41 +03:00
Maxim Devaev
93969f487c Issue #115: Comment about rtp_wrap_h264() 2021-08-13 10:39:24 +03:00
Maxim Devaev
61407c6596 Issue #115: Avoid usage of uninitialized last_client_ts 2021-08-13 10:36:53 +03:00
Maxim Devaev
6c95a56f3a Merge pull request #119 from PCPartPicker/vcpr_h264_rtp_mark_fix
Fix h264 rtp packetization - the marked bit should only be set for th…
2021-08-05 01:46:22 +03:00
aggieNick02
283f53a961 Fix h264 rtp packetization - the marked bit should only be set for the very last packet of each access unit (frame) - previously it was being set for the last packet of each type 1-5 NALU, which is equivalent and correct if the frame contains only one type 1-5 NALU, but not if it contains more 2021-08-03 23:10:21 -05:00
Maxim Devaev
ef8fb8216e .editorconfig 2021-07-25 04:23:24 +03:00
Devaev Maxim
6293d54296 Bump version: 4.3 → 4.4 2021-06-10 19:32:24 +03:00
Devaev Maxim
c20754e62c Bump version: 4.2 → 4.3 2021-06-10 19:10:51 +03:00
Devaev Maxim
c621e36929 fixed deps 2021-06-10 19:10:13 +03:00
Devaev Maxim
484ff0f32b Bump version: 4.1 → 4.2 2021-06-10 19:06:50 +03:00
Devaev Maxim
adb60124fb compile janus if provided 2021-06-10 19:06:12 +03:00
Devaev Maxim
767d8ac240 Bump version: 4.0 → 4.1 2021-06-10 18:56:00 +03:00
Devaev Maxim
4648ecba17 fixed install 2021-06-10 18:55:19 +03:00
Devaev Maxim
0a08ca657d Bump version: 3.27 → 4.0 2021-06-10 18:48:00 +03:00
Devaev Maxim
48f35ccd32 janus plugin 2021-06-10 18:42:16 +03:00
Devaev Maxim
3558089a22 get_now_monotonic_u64() 2021-06-08 18:55:52 +03:00
Devaev Maxim
47b735c51f Bump version: 3.26 → 3.27 2021-04-26 20:37:27 +03:00
Maxim Devaev
e60991974e Merge pull request #109 from netbr/master
Option to rotate image for Raspberry Pi Camera Module
2021-04-24 01:31:47 +03:00
netbr
3a16d17f8f Update options.c 2021-04-21 07:57:16 +02:00
netbr
08ddb351c7 Update device.h 2021-04-21 07:53:14 +02:00
netbr
3f251a2459 Update device.c 2021-04-21 07:52:06 +02:00
Devaev Maxim
a97f08eac8 Bump version: 3.25 → 3.26 2021-04-20 19:35:49 +03:00
Devaev Maxim
92ff097d7e show memsink has_client in the /state 2021-04-08 08:42:53 +03:00
Devaev Maxim
0eb0370bd3 moved python sources 2021-04-08 05:19:19 +03:00
Devaev Maxim
b329dfed1f Bump version: 3.24 → 3.25 2021-04-07 00:46:39 +03:00
Devaev Maxim
62dc2e5ad5 Fixed #105: using $(MAKE) 2021-04-06 23:43:32 +03:00
Devaev Maxim
7ea81fa627 fix 2021-04-03 06:42:33 +03:00
Devaev Maxim
8ddf77a3d3 Bump version: 3.23 → 3.24 2021-04-03 02:12:22 +03:00
Devaev Maxim
fde89465ac global variables prefix 2021-04-02 13:31:34 +03:00
Devaev Maxim
aa1d78a3cd fix 2021-04-02 12:39:09 +03:00
Devaev Maxim
6dfe077775 refactoring 2021-04-02 12:38:38 +03:00
Devaev Maxim
dd6dc866a6 Fixed #103: symlinks without .bin suffix 2021-04-02 09:01:29 +03:00
Devaev Maxim
3bf68884f5 improved linting 2021-04-02 02:18:27 +03:00
Devaev Maxim
e1b2eceea5 split makefiles 2021-04-02 01:00:22 +03:00
Devaev Maxim
7ce11fecb6 RN 2021-04-01 13:14:58 +03:00
Devaev Maxim
cbc6145977 using strerror_l() 2021-04-01 10:21:12 +03:00
Devaev Maxim
24f7fb797b refactoring 2021-04-01 09:32:29 +03:00
Devaev Maxim
71f1d397bf moved errno_to_string() to the tools 2021-04-01 08:49:18 +03:00
Devaev Maxim
00e83c155e removed managed flag 2021-03-30 10:16:55 +03:00
Devaev Maxim
1f186a0afe refactoring 2021-03-30 10:11:14 +03:00
Devaev Maxim
3c7075d0d2 removed frame name 2021-03-30 10:02:03 +03:00
Devaev Maxim
c1d7bd1595 link fix 2021-03-30 03:21:04 +03:00
Devaev Maxim
04d1d3d5a6 some common macroses 2021-03-30 02:18:40 +03:00
Devaev Maxim
01bc0529e7 refactoring 2021-03-28 22:48:17 +03:00
Devaev Maxim
181231f3ff common list operations 2021-03-28 21:51:34 +03:00
Devaev Maxim
58569f0315 separate major and minor numbers 2021-03-28 08:04:12 +03:00
Maxim Devaev
5903fcf718 Update README.ru.md 2021-03-27 22:45:24 +03:00
Maxim Devaev
1a820b23a5 Merge pull request #101 from Lennie/patch-1
Update README.md
2021-03-27 22:44:51 +03:00
Lennie
67b767b152 Update README.md
On my system I needed to use -d my guess is it was just a typo from you ?
2021-03-27 20:34:33 +01:00
Devaev Maxim
74bf710bb6 Bump version: 3.22 → 3.23 2021-03-26 22:03:40 +03:00
Devaev Maxim
1d3b428a75 using asprintf 2021-03-25 14:23:43 +03:00
Devaev Maxim
749bc5caf7 const 2021-03-25 13:35:56 +03:00
Devaev Maxim
3b7cbc62c4 Bump version: 3.21 → 3.22 2021-03-22 07:07:56 +03:00
Devaev Maxim
dff49d8e7b h264 mmal brokes on 0 fps and 640x480 2021-03-22 07:07:21 +03:00
Maxim Devaev
b23883e65f Update README.md 2021-03-21 17:57:37 +03:00
Maxim Devaev
c2e30c7fc4 Update README.ru.md 2021-03-21 17:57:01 +03:00
Devaev Maxim
37216250b8 Bump version: 3.20 → 3.21 2021-03-21 03:58:35 +03:00
Devaev Maxim
dc82894038 Issue #100: handle X-Forwarded-For 2021-03-21 03:35:47 +03:00
Devaev Maxim
f3a350148e Bump version: 3.19 → 3.20 2021-03-14 18:43:53 +03:00
Devaev Maxim
5e49f50e22 don't cache dict_frame 2021-03-14 18:16:41 +03:00
Devaev Maxim
91ff97f721 Bump version: 3.18 → 3.19 2021-03-14 03:03:54 +03:00
Devaev Maxim
daaa488dd6 segfault fix 2021-03-14 03:03:18 +03:00
Devaev Maxim
bad05a6827 Bump version: 3.17 → 3.18 2021-03-07 16:35:22 +03:00
Devaev Maxim
89d3ed98c7 readme fix 2021-03-07 16:23:22 +03:00
Devaev Maxim
1187ace2a2 removed libuuid dep and using get_now_id() 2021-03-07 14:54:13 +03:00
Devaev Maxim
9704fb054c fixed pikvm/pikvm#222 2021-03-06 13:49:08 +03:00
Maxim Devaev
b10148a438 Update README.md 2021-03-01 07:32:00 +03:00
Devaev Maxim
1b4bcd09fe Bump version: 3.16 → 3.17 2021-02-18 06:34:26 +03:00
Devaev Maxim
245cd94a4c ru readme fix 2021-02-18 04:07:56 +03:00
Devaev Maxim
8a24a0b129 prevent hanging on vcos semaphore 2021-02-18 03:38:48 +03:00
Devaev Maxim
b362c1fa50 russian readme 2021-02-17 23:25:17 +03:00
Maxim Devaev
0414fa5a51 Merge pull request #94 from Drachenkaetzchen/patch-1
Give V4L2_PIX_FMT_MJPEG an unique name
2021-02-17 23:17:06 +03:00
Maxim Devaev
191dd16d7b Merge pull request #91 from mtlynch/trailing-whitespace
Delete trailing whitespace from README
2021-02-17 23:15:11 +03:00
Maxim Devaev
31a03928c9 Merge pull request #90 from mtlynch/libjpeg-dev-ubuntu
Add libjpeg-dev to Ubuntu 20.04 dependencies
2021-02-17 23:14:50 +03:00
Felicia Hummel
185a8b6773 Update README.md 2021-02-17 11:48:16 +01:00
Felicia Hummel
b7501cfbda Add MJPEG to FORMATS_STR
Add missing MJPEG option to FORMATS_STR
2021-02-12 00:17:06 +01:00
Felicia Hummel
1dc90dad7a Give V4L2_PIX_FMT_MJPEG an unique name
This patch gives `V4L2_PIX_FMT_MJPEG` an unique name. Previously, `V4L2_PIX_FMT_MJPEG` would be chosen instead of `V4L2_PIX_FMT_JPEG` if `JPEG` was specified on the command line.

Relates to issue #40
2021-02-12 00:06:03 +01:00
Michael Lynch
45a4d148f5 Delete trailing whitespace from README 2021-02-05 15:19:10 -05:00
Michael Lynch
93ab12b54b Add libjpeg-dev to Ubuntu 20.04 dependencies
Building on Ubuntu 20.04 fails without libjpeg-dev, so this change adds it to the README.
2021-02-05 15:18:26 -05:00
Devaev Maxim
fb07444b70 Bump version: 3.15 → 3.16 2021-02-04 01:55:16 +03:00
Devaev Maxim
a8ba2f7364 H264: disable MMAL_PARAMETER_VIDEO_ENCODE_FRAME_LIMIT_BITS 2021-02-04 01:54:42 +03:00
Devaev Maxim
171bcb315a Bump version: 3.14 → 3.15 2021-02-03 05:49:40 +03:00
Devaev Maxim
911df1af15 rename 2021-02-03 05:48:15 +03:00
Devaev Maxim
a165ff4523 expose keyframe flag 2021-02-03 05:39:18 +03:00
Devaev Maxim
5cfb3b1e60 Bump version: 3.13 → 3.14 2021-01-28 22:16:44 +03:00
Devaev Maxim
82b3b78238 ignore drop-same-frames for sinks 2021-01-27 11:30:35 +03:00
Devaev Maxim
8a82ff6691 using archlinux/archlinux:base-devel 2021-01-25 11:58:52 +03:00
Devaev Maxim
2807678551 Bump version: 3.12 → 3.13 2021-01-25 03:26:26 +03:00
Devaev Maxim
e96e0aa73c workaround for unreasonable rebuilding in package() 2021-01-25 03:21:31 +03:00
Devaev Maxim
d06c2619b2 added missing options to man 2021-01-24 12:26:43 +03:00
Devaev Maxim
6b18455d11 systemd-tmpfiles hangs 2021-01-24 12:21:08 +03:00
Maxim Devaev
217977d9cb Merge pull request #87 from reedy/patch-1
Fix casing of macros
2021-01-22 05:22:26 +03:00
Maxim Devaev
b9f186e47c Merge pull request #88 from reedy/patch-2
Fix below typo
2021-01-22 05:21:44 +03:00
Sam Reed
d7d56f3536 Fix below typo 2021-01-22 01:45:40 +00:00
Sam Reed
2e3c764369 Fix casing of macros 2021-01-22 01:43:50 +00:00
Devaev Maxim
7236e53813 Bump version: 3.11 → 3.12 2021-01-22 02:00:35 +03:00
Devaev Maxim
e7f7350405 fix 2021-01-21 23:49:37 +03:00
Devaev Maxim
81f0266a87 drop_same_frames in python 2021-01-21 23:37:06 +03:00
Devaev Maxim
eb1a2e3695 Bump version: 3.10 → 3.11 2021-01-21 18:55:17 +03:00
Devaev Maxim
6cfd3739d3 fixed python context manager 2021-01-21 18:54:39 +03:00
Devaev Maxim
77b8386de7 Bump version: 3.9 → 3.10 2021-01-21 16:59:47 +03:00
Devaev Maxim
a002bd3427 Makefile deps fix 2021-01-21 16:59:10 +03:00
Devaev Maxim
202b907430 Bump version: 3.8 → 3.9 2021-01-21 12:47:40 +03:00
Devaev Maxim
d9f4aba953 fixed build 2021-01-21 12:47:05 +03:00
Devaev Maxim
1b08857534 Bump version: 3.7 → 3.8 2021-01-21 12:03:35 +03:00
Devaev Maxim
eec19892fa fixed PKGBUILD 2021-01-21 12:03:01 +03:00
Devaev Maxim
40abe73391 Bump version: 3.6 → 3.7 2021-01-21 11:51:34 +03:00
Devaev Maxim
e4f1ef654f lint fix 2021-01-21 11:34:24 +03:00
Devaev Maxim
fa6afb96ce check usleep() retval 2021-01-21 11:22:04 +03:00
Devaev Maxim
c3f98b34f2 python builddeps 2021-01-21 09:27:29 +03:00
Devaev Maxim
b7b3e8e87d python: handle signals 2021-01-21 09:18:09 +03:00
Devaev Maxim
94383a2d54 moved python module 2021-01-21 07:19:10 +03:00
Devaev Maxim
184a4879eb gzip force 2021-01-21 05:23:22 +03:00
Devaev Maxim
bf48908c59 fixed another segfault 2021-01-21 05:22:35 +03:00
Devaev Maxim
66afbccf21 fixed segfault 2021-01-21 03:01:50 +03:00
Devaev Maxim
97dbe59aea handle usleep error 2021-01-21 02:56:43 +03:00
Devaev Maxim
d874fdeaec gitignore 2021-01-20 16:06:42 +03:00
Devaev Maxim
97e3938d56 trying to eliminate memory leaks 2021-01-20 16:06:32 +03:00
Devaev Maxim
87c7e8063f fixed shm umask 2021-01-20 15:17:13 +03:00
Devaev Maxim
fe5beb0114 fixed python destdir 2021-01-20 14:24:12 +03:00
Devaev Maxim
eec8e41b2b Bump version: 3.5 → 3.6 2021-01-20 14:03:48 +03:00
Devaev Maxim
87bff56a78 python module 2021-01-20 14:03:08 +03:00
Devaev Maxim
34e0e4dab4 moved flock_timedwait_monotonic() to tools.h 2021-01-20 12:36:37 +03:00
Devaev Maxim
e08ac1467f moved memsink_shared_s to separate file 2021-01-20 12:28:46 +03:00
Devaev Maxim
bc25e787cc buf fix 2021-01-20 12:27:28 +03:00
Devaev Maxim
5f11caf6fc added protocol version to memsink 2021-01-20 02:21:03 +03:00
Devaev Maxim
9be264e176 memsink magic 2021-01-18 11:27:57 +03:00
Devaev Maxim
3d28dcbaff improved memsink logic 2021-01-18 11:05:04 +03:00
Devaev Maxim
61c3b44c8a raw sink 2021-01-17 14:33:18 +03:00
Devaev Maxim
e26973a9f1 don't show help on option error 2021-01-17 09:37:44 +03:00
Devaev Maxim
598e2372e5 refactoring 2021-01-17 09:35:55 +03:00
Devaev Maxim
4fb8c7745c client ttl; some refactoring 2021-01-17 08:38:13 +03:00
Devaev Maxim
14131f0b54 check memsink clients 2021-01-17 00:06:44 +03:00
Devaev Maxim
de41c9653e pluggable outputs for the future 2021-01-15 12:38:32 +03:00
80 changed files with 2706 additions and 707 deletions

View File

@@ -1,14 +1,26 @@
[bumpversion]
commit = True
tag = True
current_version = 3.5
parse = (?P<major>\d+)\.(?P<minor>\d+)(\.(?P<patch>\d+)(\-(?P<release>[a-z]+))?)?
current_version = 4.11
parse = (?P<major>\d+)\.(?P<minor>\d+)
serialize =
{major}.{minor}
[bumpversion:file:src/libs/config.h]
search = VERSION "{current_version}"
replace = VERSION "{new_version}"
parse = (?P<major>\d+)
serialize = {major}
search = VERSION_MAJOR {current_version}
replace = VERSION_MAJOR {new_version}
[bumpversion:file:./src/libs/config.h]
parse = <major>\d+\.(?P<minor>\d+)
serialize = {minor}
search = VERSION_MINOR {current_version}
replace = VERSION_MINOR {new_version}
[bumpversion:file:python/setup.py]
search = version="{current_version}"
replace = version="{new_version}"
[bumpversion:file:pkg/arch/PKGBUILD]
search = pkgver={current_version}

View File

@@ -4,3 +4,6 @@
# Allow source code
!Makefile
!src/**
!python/**
!janus/**
!man/**

10
.editorconfig Normal file
View File

@@ -0,0 +1,10 @@
root = true
[*]
end_of_file = lf
indent_style = tab
indent_size = 4
[*.{py,yaml}]
indent_style = space
indent_size = 4

18
.gitignore vendored
View File

@@ -2,12 +2,16 @@
/linters/.mypy_cache/
/pkg/arch/pkg/
/pkg/arch/src/
/pkg/arch/v*.tar.gz
/pkg/arch/ustreamer-*.pkg.tar.xz
/pkg/arch/ustreamer-*.pkg.tar.zst
/build/
/config.mk
/vgcore.*
/src/build/
/python/build/
/janus/build/
/ustreamer
/ustreamer-dump
/*.sock
/config.mk
vgcore.*
*.sock
*.so
*.bin
*.pkg.tar.xz
*.pkg.tar.zst
*.tar.gz

179
Makefile
View File

@@ -1,153 +1,93 @@
-include config.mk
USTR ?= ustreamer
DUMP ?= ustreamer-dump
DESTDIR ?=
PREFIX ?= /usr/local
MANPREFIX ?= $(PREFIX)/share/man
CC ?= gcc
CFLAGS ?= -O3 -MD
PY ?= python3
CFLAGS ?= -O3
LDFLAGS ?=
RPI_VC_HEADERS ?= /opt/vc/include
RPI_VC_LIBS ?= /opt/vc/lib
BUILD ?= build
export
LINTERS_IMAGE ?= $(USTR)-linters
_LINTERS_IMAGE ?= ustreamer-linters
# =====
override CFLAGS += -c -std=c11 -Wall -Wextra -D_GNU_SOURCE
_COMMON_LIBS = -lm -ljpeg -pthread -lrt
_USTR_LIBS = $(_COMMON_LIBS) -levent -levent_pthreads -luuid
_USTR_SRCS = $(shell ls \
src/libs/*.c \
src/ustreamer/*.c \
src/ustreamer/http/*.c \
src/ustreamer/data/*.c \
src/ustreamer/encoders/cpu/*.c \
src/ustreamer/encoders/hw/*.c \
)
_DUMP_LIBS = $(_COMMON_LIBS)
_DUMP_SRCS = $(shell ls \
src/libs/*.c \
src/dump/*.c \
)
define optbool
$(filter $(shell echo $(1) | tr A-Z a-z), yes on 1)
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)
_USTR_SRCS += $(shell ls \
src/ustreamer/encoders/omx/*.c \
src/ustreamer/h264/*.c \
)
endif
ifneq ($(call optbool,$(WITH_GPIO)),)
_USTR_LIBS += -lgpiod
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
endif
WITH_SETPROCTITLE ?= 1
ifneq ($(call optbool,$(WITH_SETPROCTITLE)),)
ifeq ($(shell uname -s | tr A-Z a-z),linux)
_USTR_LIBS += -lbsd
endif
override CFLAGS += -DWITH_SETPROCTITLE
endif
# =====
all: $(USTR) $(DUMP)
all:
+ $(MAKE) apps
ifneq ($(call optbool,$(WITH_PYTHON)),)
+ $(MAKE) python
endif
ifneq ($(call optbool,$(WITH_JANUS)),)
+ $(MAKE) janus
endif
install: $(USTR) $(DUMP)
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
apps:
$(MAKE) -C src
@ ln -sf src/ustreamer.bin ustreamer
@ ln -sf src/ustreamer-dump.bin ustreamer-dump
python:
$(MAKE) -C python
@ ln -sf python/build/lib.*/*.so .
janus:
$(MAKE) -C janus
@ ln -sf janus/*.so .
install: all
$(MAKE) -C src install
ifneq ($(call optbool,$(WITH_PYTHON)),)
$(MAKE) -C python install
endif
ifneq ($(call optbool,$(WITH_JANUS)),)
$(MAKE) -C janus install
endif
mkdir -p $(DESTDIR)$(MANPREFIX)/man1
for man in $(shell ls man); do \
install -m644 man/$$man $(DESTDIR)$(MANPREFIX)/man1/$$man; \
gzip -f $(DESTDIR)$(MANPREFIX)/man1/$$man; \
done
install-strip: install
strip $(DESTDIR)$(PREFIX)/bin/$(USTR)
strip $(DESTDIR)$(PREFIX)/bin/$(DUMP)
uninstall:
rm -f $(DESTDIR)$(PREFIX)/bin/$(USTR) \
$(DESTDIR)$(PREFIX)/bin/$(DUMP) \
$(DESTDIR)$(MANPREFIX)/man1/$(USTR).1 \
$(DESTDIR)$(MANPREFIX)/man1/$(DUMP).1
$(MAKE) -C src install-strip
regen:
tools/make-jpeg-h.py src/ustreamer/data/blank.jpeg src/ustreamer/data/blank_jpeg.c BLANK
tools/make-html-h.py src/ustreamer/data/index.html src/ustreamer/data/index_html.c INDEX
$(USTR): $(_USTR_SRCS:%.c=$(BUILD)/%.o)
# $(info ========================================)
$(info == LD $@)
@ $(CC) $^ -o $@ $(LDFLAGS) $(_USTR_LIBS)
# $(info :: CC = $(CC))
# $(info :: LIBS = $(_USTR_LIBS))
# $(info :: CFLAGS = $(CFLAGS))
# $(info :: LDFLAGS = $(LDFLAGS))
$(DUMP): $(_DUMP_SRCS:%.c=$(BUILD)/%.o)
# $(info ========================================)
$(info == LD $@)
@ $(CC) $^ -o $@ $(LDFLAGS) $(_DUMP_LIBS)
# $(info :: CC = $(CC))
# $(info :: LIBS = $(_DUMP_LIBS))
# $(info :: CFLAGS = $(CFLAGS))
# $(info :: LDFLAGS = $(LDFLAGS))
$(BUILD)/%.o: %.c
$(info -- CC $<)
@ mkdir -p $(dir $@) || true
@ $(CC) $< -o $@ $(CFLAGS)
tools/$(MAKE)-jpeg-h.py src/ustreamer/data/blank.jpeg src/ustreamer/data/blank_jpeg.c BLANK
tools/$(MAKE)-html-h.py src/ustreamer/data/index.html src/ustreamer/data/index_html.c INDEX
release:
make clean
make tox
make push
make bump V=$(V)
make push
make clean
$(MAKE) clean
$(MAKE) tox
$(MAKE) push
$(MAKE) bump V=$(V)
$(MAKE) push
$(MAKE) clean
tox: linters
time docker run --rm \
--volume `pwd`:/src:ro \
--volume `pwd`/linters:/src/linters:rw \
-t $(LINTERS_IMAGE) bash -c " \
-t $(_LINTERS_IMAGE) bash -c " \
cd /src \
&& tox -q -c linters/tox.ini $(if $(E),-e $(E),-p auto) \
"
@@ -157,7 +97,7 @@ linters:
docker build \
$(if $(call optbool,$(NC)),--no-cache,) \
--rm \
--tag $(LINTERS_IMAGE) \
--tag $(_LINTERS_IMAGE) \
-f linters/Dockerfile linters
@@ -173,14 +113,15 @@ push:
clean-all: linters clean
- docker run --rm \
--volume `pwd`:/src \
-it $(LINTERS_IMAGE) bash -c "cd src && rm -rf linters/{.tox,.mypy_cache}"
-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) vgcore.* *.sock
rm -f ustreamer ustreamer-dump *.so
$(MAKE) -C src clean
$(MAKE) -C python clean
$(MAKE) -C janus clean
.PHONY: linters
_OBJS = $(_USTR_SRCS:%.c=$(BUILD)/%.o) $(_DUMP_SRCS:%.c=$(BUILD)/%.o)
-include $(_OBJS:%.o=%.d)
.PHONY: python janus linters

View File

@@ -11,21 +11,21 @@
| **Feature** | **µStreamer** | **mjpg-streamer** |
|----------|---------------|-------------------|
| Multithreaded JPEG encoding | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Yes | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) No |
| [OpenMAX IL](https://www.khronos.org/openmaxil) hardware acceleration<br>on Raspberry Pi | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Yes | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) No |
| Behavior when the device<br>is disconnected while streaming | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Shows a black screen<br>with ```NO SIGNAL``` on it<br>until reconnected | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) 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 | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Yes | ![#ffaa00](https://placehold.it/15/ffaa00/000000?text=+) Partially yes <sup>1</sup> |
| Option to skip frames when streaming<br>static images by HTTP to save the traffic | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Yes <sup>2</sup> | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) No |
| Streaming via UNIX domain socket | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Yes | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) No |
| Debug logs without recompiling,<br>performance statistics log,<br>access to HTTP streaming parameters | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Yes | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) No |
| Option to serve files<br>with a built-in HTTP server | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Yes | ![#ffaa00](https://placehold.it/15/ffaa00/000000?text=+) Regular files only |
| Signaling about the stream state<br>on GPIO using [libgpiod](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/about) | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Yes | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) No |
| Access to webcam controls (focus, servos)<br>and settings such as brightness via HTTP | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) No | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Yes |
| Compatibility with mjpg-streamer's API | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) 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,11 @@ 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-dev` for `WITH_GPIO=1`.
* Debian/Ubuntu: `sudo apt install build-essential libevent-dev libjpeg-dev libbsd-dev`.
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 +49,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,10 +76,52 @@ $ ./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
```
-----
# Integrations
## Nginx
When uStreamer is behind an Nginx proxy, it's buffering behavior introduces latency into the video stream. It's possible to disable Nginx's buffering to eliminate the additional latency:
```nginx
location /stream {
postpone_output 0;
proxy_buffering off;
proxy_ignore_headers X-Accel-Buffering;
proxy_pass http://ustreamer;
}
```
-----
# 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).
* [uStreamer Ansible Role](https://github.com/mtlynch/ansible-role-ustreamer): Use [Ansible](https://docs.ansible.com/ansible/latest/index.html) to compile uStreamer and install it as a systemd service automatically.
-----
# License

View File

@@ -11,21 +11,21 @@
| **Фича** | **µStreamer** | **mjpg-streamer** |
|----------|---------------|-------------------|
| Многопоточное кодирование JPEG | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет |
| Аппаратное кодирование с помощью [OpenMAX IL](https://www.khronos.org/openmaxil) на Raspberry Pi | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет |
| Поведение при физическом отключении<br>устройства от сервера во время работы | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Транслирует черный экран<br>с надписью ```NO SIGNAL```,<br>пока устройство не будет подключено снова | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Прерывает трансляцию <sup>1</sup> |
| Поддержка [DV-таймингов](https://linuxtv.org/downloads/v4l-dvb-apis-new/userspace-api/v4l/dv-timings.html) - возможности<br>изменения параметров разрешения<br>трансляции на лету по сигналу<br>источника (устройства видеозахвата) | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть | ![#ffaa00](https://placehold.it/15/ffaa00/000000?text=+) Условно есть <sup>1</sup> |
| Возможность пропуска фреймов при передаче<br>статического изображения по HTTP<br>для экономии трафика | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть <sup>2</sup> | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет |
| Стрим через UNIX domain socket | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет |
| Дебаг-логи без перекомпиляции,<br>логгирование статистики производительности,<br>возможность получения параметров<br>трансляции по HTTP | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет |
| Возможность сервить файлы встроенным<br>HTTP-сервером | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть | ![#ffaa00](https://placehold.it/15/ffaa00/000000?text=+) Нет каталогов |
| Вывод сигналов о состоянии стрима на GPIO<br>с помощью [libgpiod](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/about) | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет |
| Поддержка контролов веб-камер (фокус,<br> движение сервами) и всяких настроек,<br> типа яркости, через HTTP | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть |
| Совместимость с API mjpg-streamer'а | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть | :) |
| Многопоточное кодирование 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,11 @@
-----
# Сборка
Для сборки вам понадобятся ```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/Ubuntu: `sudo apt install build-essential libevent-dev libjpeg-dev libbsd-dev`.
На 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,10 +76,52 @@ $ ./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
```
-----
# Интеграция
## Nginx
Если uStreamer находится на Nginx, то последний будет буферизировать поток и создавать дополнительную задержку в стриме. Чтобы задержки не было, буферизацию можно отключить:
```nginx
location /stream {
postpone_output 0;
proxy_buffering off;
proxy_ignore_headers X-Accel-Buffering;
proxy_pass http://ustreamer;
}
```
-----
# Утилиты 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).
* [uStreamer Ansible Role](https://github.com/mtlynch/ansible-role-ustreamer): Использование [Ansible](https://docs.ansible.com/ansible/latest/index.html) для сборки и установки стримера как systemd-сервиса.
-----
# Лицензия

54
janus/Makefile Normal file
View File

@@ -0,0 +1,54 @@
DESTDIR ?=
PREFIX ?= /usr/local
CC ?= gcc
CFLAGS ?= -O3
LDFLAGS ?=
# =====
_PLUGIN = libjanus_ustreamer.so
_CFLAGS = -fPIC -MD -c -std=c11 -Wall -Wextra -D_GNU_SOURCE $(shell pkg-config --cflags glib-2.0) $(CFLAGS)
_LDFLAGS = -shared -lm -pthread -lrt -ljansson $(shell pkg-config --libs glib-2.0) $(LDFLAGS)
_SRCS = $(shell ls src/*.c)
_BUILD = build
define optbool
$(filter $(shell echo $(1) | tr A-Z a-z), yes on 1)
endef
WITH_PTHREAD_NP ?= 1
ifneq ($(call optbool,$(WITH_PTHREAD_NP)),)
override _CFLAGS += -DWITH_PTHREAD_NP
endif
# =====
$(_PLUGIN): $(_SRCS:%.c=$(_BUILD)/%.o)
$(info == SO $@)
@ $(CC) $^ -o $@ $(_LDFLAGS)
$(_BUILD)/%.o: %.c
$(info -- CC $<)
@ mkdir -p $(dir $@) || true
@ $(CC) $< -o $@ $(_CFLAGS)
install: $(_PLUGIN)
mkdir -p $(DESTDIR)$(PREFIX)/lib/ustreamer/janus
install -m755 $(_PLUGIN) $(DESTDIR)$(PREFIX)/lib/ustreamer/janus/$(PLUGIN)
clean:
rm -rf $(_PLUGIN) $(_BUILD)
_OBJS = $(_SRCS:%.c=$(_BUILD)/%.o)
-include $(_OBJS:%.o=%.d)

1
janus/src/base64.c Symbolic link
View File

@@ -0,0 +1 @@
../../src/libs/base64.c

1
janus/src/base64.h Symbolic link
View File

@@ -0,0 +1 @@
../../src/libs/base64.h

1
janus/src/config.h Symbolic link
View File

@@ -0,0 +1 @@
../../src/libs/config.h

1
janus/src/frame.c Symbolic link
View File

@@ -0,0 +1 @@
../../src/libs/frame.c

1
janus/src/frame.h Symbolic link
View File

@@ -0,0 +1 @@
../../src/libs/frame.h

1
janus/src/list.h Symbolic link
View File

@@ -0,0 +1 @@
../../src/libs/list.h

1
janus/src/memsinksh.h Symbolic link
View File

@@ -0,0 +1 @@
../../src/libs/memsinksh.h

522
janus/src/plugin.c Normal file
View File

@@ -0,0 +1,522 @@
/*****************************************************************************
# #
# 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 <stdint.h>
#include <stdbool.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <assert.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <linux/videodev2.h>
#include <pthread.h>
#include <jansson.h>
#include <janus/config.h>
#include <janus/plugins/plugin.h>
#include "config.h"
#include "tools.h"
#include "threading.h"
#include "list.h"
#include "memsinksh.h"
#include "rtp.h"
static int _plugin_init(janus_callbacks *gw, const char *config_file_path);
static void _plugin_destroy(void);
static void _plugin_create_session(janus_plugin_session *session, int *error);
static void _plugin_destroy_session(janus_plugin_session *session, int *error);
static json_t *_plugin_query_session(janus_plugin_session *session);
static void _plugin_setup_media(janus_plugin_session *session);
static void _plugin_hangup_media(janus_plugin_session *session);
static struct janus_plugin_result *_plugin_handle_message(
janus_plugin_session *session, char *transaction, json_t *msg, json_t *jsep);
static int _plugin_get_api_compatibility(void);
static int _plugin_get_version(void);
static const char *_plugin_get_version_string(void);
static const char *_plugin_get_description(void);
static const char *_plugin_get_name(void);
static const char *_plugin_get_author(void);
static const char *_plugin_get_package(void);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Woverride-init"
static janus_plugin _plugin = JANUS_PLUGIN_INIT(
.init = _plugin_init,
.destroy = _plugin_destroy,
.create_session = _plugin_create_session,
.destroy_session = _plugin_destroy_session,
.query_session = _plugin_query_session,
.setup_media = _plugin_setup_media,
.hangup_media = _plugin_hangup_media,
.handle_message = _plugin_handle_message,
.get_api_compatibility = _plugin_get_api_compatibility,
.get_version = _plugin_get_version,
.get_version_string = _plugin_get_version_string,
.get_description = _plugin_get_description,
.get_name = _plugin_get_name,
.get_author = _plugin_get_author,
.get_package = _plugin_get_package,
);
#pragma GCC diagnostic pop
janus_plugin *create(void) { // cppcheck-suppress unusedFunction
return &_plugin;
}
typedef struct _client_sx {
janus_plugin_session *session;
bool transmit;
LIST_STRUCT(struct _client_sx);
} _client_s;
static char *_g_memsink_obj = NULL;
const long double _g_wait_timeout = 1;
const long double _g_lock_timeout = 1;
const useconds_t _g_lock_polling = 1000;
const useconds_t _g_watchers_polling = 100000;
static _client_s *_g_clients = NULL;
static janus_callbacks *_g_gw = NULL;
static rtp_s *_g_rtp = NULL;
static pthread_t _g_tid;
static pthread_mutex_t _g_lock = PTHREAD_MUTEX_INITIALIZER;
static atomic_bool _g_ready = ATOMIC_VAR_INIT(false);
static atomic_bool _g_stop = ATOMIC_VAR_INIT(false);
static atomic_bool _g_has_watchers = ATOMIC_VAR_INIT(false);
#define JLOG_INFO(_msg, ...) JANUS_LOG(LOG_INFO, "== %s -- " _msg "\n", _plugin_get_name(), ##__VA_ARGS__)
#define JLOG_WARN(_msg, ...) JANUS_LOG(LOG_WARN, "== %s -- " _msg "\n", _plugin_get_name(), ##__VA_ARGS__)
#define JLOG_ERROR(_msg, ...) JANUS_LOG(LOG_ERR, "== %s -- " _msg "\n", _plugin_get_name(), ##__VA_ARGS__)
#define JLOG_PERROR(_msg, ...) { \
char _perror_buf[1024] = {0}; \
char *_perror_ptr = errno_to_string(errno, _perror_buf, 1023); \
JANUS_LOG(LOG_ERR, "[%s] " _msg ": %s\n", _plugin_get_name(), ##__VA_ARGS__, _perror_ptr); \
}
#define LOCK A_MUTEX_LOCK(&_g_lock)
#define UNLOCK A_MUTEX_UNLOCK(&_g_lock)
#define READY atomic_load(&_g_ready)
#define STOP atomic_load(&_g_stop)
#define HAS_WATCHERS atomic_load(&_g_has_watchers)
static void _relay_rtp_clients(const uint8_t *datagram, size_t size) {
janus_plugin_rtp packet = {
.video = true,
.buffer = (char *)datagram,
.length = size,
};
janus_plugin_rtp_extensions_reset(&packet.extensions);
LIST_ITERATE(_g_clients, client, {
if (client->transmit) {
_g_gw->relay_rtp(client->session, &packet);
}
});
}
static int _wait_frame(int fd, memsink_shared_s* mem, uint64_t last_id) {
long double deadline_ts = get_now_monotonic() + _g_wait_timeout;
long double now;
do {
int retval = flock_timedwait_monotonic(fd, _g_lock_timeout);
now = get_now_monotonic();
if (retval < 0 && errno != EWOULDBLOCK) {
JLOG_PERROR("Can't lock memsink");
return -1;
} else if (retval == 0) {
if (mem->magic == MEMSINK_MAGIC && mem->version == MEMSINK_VERSION && mem->id != last_id) {
return 0;
}
if (flock(fd, LOCK_UN) < 0) {
JLOG_PERROR("Can't unlock memsink");
return -1;
}
}
usleep(_g_lock_polling);
} while (now < deadline_ts);
return -2;
}
static int _get_frame(int fd, memsink_shared_s *mem, frame_s *frame, uint64_t *frame_id) {
frame_set_data(frame, mem->data, mem->used);
FRAME_COPY_META(mem, frame);
*frame_id = mem->id;
mem->last_client_ts = get_now_monotonic();
int retval = 0;
if (frame->format != V4L2_PIX_FMT_H264) {
JLOG_ERROR("Got non-H264 frame from memsink");
retval = -1;
}
if (flock(fd, LOCK_UN) < 0) {
JLOG_PERROR("Can't unlock memsink");
retval = -1;
}
return retval;
}
static void *_clients_thread(UNUSED void *arg) {
A_THREAD_RENAME("us_clients");
atomic_store(&_g_ready, true);
frame_s *frame = frame_init();
uint64_t frame_id = 0;
unsigned error_reported = 0;
# define IF_NOT_REPORTED(_error, ...) { \
if (error_reported != _error) { __VA_ARGS__; error_reported = _error; } \
}
while (!STOP) {
if (!HAS_WATCHERS) {
IF_NOT_REPORTED(1, {
JLOG_INFO("No active watchers, memsink disconnected");
});
usleep(_g_watchers_polling);
continue;
}
int fd = -1;
memsink_shared_s *mem = NULL;
if ((fd = shm_open(_g_memsink_obj, O_RDWR, 0)) <= 0) {
IF_NOT_REPORTED(2, {
JLOG_PERROR("Can't open memsink");
});
goto close_memsink;
}
if ((mem = memsink_shared_map(fd)) == NULL) {
IF_NOT_REPORTED(3, {
JLOG_PERROR("Can't map memsink");
});
goto close_memsink;
}
error_reported = 0;
JLOG_INFO("Memsink opened; reading frames ...");
while (!STOP && HAS_WATCHERS) {
int result = _wait_frame(fd, mem, frame_id);
if (result == 0) {
if (_get_frame(fd, mem, frame, &frame_id) != 0) {
goto close_memsink;
}
LOCK;
rtp_wrap_h264(_g_rtp, frame, _relay_rtp_clients);
UNLOCK;
} else if (result == -1) {
goto close_memsink;
}
}
close_memsink:
if (mem != NULL) {
JLOG_INFO("Memsink closed");
memsink_shared_unmap(mem);
mem = NULL;
}
if (fd > 0) {
close(fd);
fd = -1;
}
sleep(1); // error_delay
}
# undef IF_NOT_REPORTED
frame_destroy(frame);
return NULL;
}
static int _read_config(const char *config_dir_path) {
char *config_file_path;
janus_config *config = NULL;
A_ASPRINTF(config_file_path, "%s/%s.jcfg", config_dir_path, _plugin_get_package());
JLOG_INFO("Reading config file '%s' ...", config_file_path);
config = janus_config_parse(config_file_path);
if (config == NULL) {
JLOG_ERROR("Can't read config");
goto error;
}
janus_config_print(config);
janus_config_category *config_memsink = janus_config_get_create(config, NULL, janus_config_type_category, "memsink");
janus_config_item *config_memsink_obj = janus_config_get(config, config_memsink, janus_config_type_item, "object");
if (config_memsink_obj == NULL || config_memsink_obj->value == NULL || config_memsink_obj->value[0] == '\0') {
JLOG_ERROR("Missing config value: memsink.object");
goto error;
}
_g_memsink_obj = strdup(config_memsink_obj->value);
int retval = 0;
goto ok;
error:
retval = -1;
ok:
if (config) {
janus_config_destroy(config);
}
free(config_file_path);
return retval;
}
static int _plugin_init(janus_callbacks *gw, const char *config_dir_path) {
// https://groups.google.com/g/meetecho-janus/c/xoWIQfaoJm8
// sysctl -w net.core.rmem_default=500000
// sysctl -w net.core.wmem_default=500000
// sysctl -w net.core.rmem_max=1000000
// sysctl -w net.core.wmem_max=1000000
JLOG_INFO("Initializing plugin ...");
assert(!READY);
assert(!STOP);
if (gw == NULL || config_dir_path == NULL || _read_config(config_dir_path) < 0) {
return -1;
}
_g_gw = gw;
_g_rtp = rtp_init();
A_THREAD_CREATE(&_g_tid, _clients_thread, NULL);
return 0;
}
static void _plugin_destroy(void) {
JLOG_INFO("Destroying plugin ...");
atomic_store(&_g_stop, true);
if (READY) {
A_THREAD_JOIN(_g_tid);
}
LIST_ITERATE(_g_clients, client, {
LIST_REMOVE(_g_clients, client);
free(client);
});
_g_clients = NULL;
rtp_destroy(_g_rtp);
_g_rtp = NULL;
_g_gw = NULL;
if (_g_memsink_obj) {
free(_g_memsink_obj);
_g_memsink_obj = NULL;
}
}
#define IF_DISABLED(...) { if (!READY || STOP) { __VA_ARGS__ } }
static void _plugin_create_session(janus_plugin_session *session, int *error) {
IF_DISABLED({ *error = -1; return; });
LOCK;
JLOG_INFO("Creating session %p ...", session);
_client_s *client;
A_CALLOC(client, 1);
client->session = session;
client->transmit = true;
LIST_APPEND(_g_clients, client);
atomic_store(&_g_has_watchers, true);
UNLOCK;
}
static void _plugin_destroy_session(janus_plugin_session* session, int *error) {
IF_DISABLED({ *error = -1; return; });
LOCK;
bool found = false;
bool has_watchers = false;
LIST_ITERATE(_g_clients, client, {
if (client->session == session) {
JLOG_INFO("Removing session %p ...", session);
LIST_REMOVE(_g_clients, client);
free(client);
found = true;
} else {
has_watchers = (has_watchers || client->transmit);
}
});
if (!found) {
JLOG_WARN("No session %p", session);
*error = -2;
}
atomic_store(&_g_has_watchers, has_watchers);
UNLOCK;
}
static json_t *_plugin_query_session(janus_plugin_session *session) {
IF_DISABLED({ return NULL; });
json_t *info = NULL;
LOCK;
LIST_ITERATE(_g_clients, client, {
if (client->session == session) {
info = json_string("session_found");
break;
}
});
UNLOCK;
return info;
}
static void _set_transmit(janus_plugin_session *session, UNUSED const char *msg, bool transmit) {
IF_DISABLED({ return; });
LOCK;
bool found = false;
bool has_watchers = false;
LIST_ITERATE(_g_clients, client, {
if (client->session == session) {
client->transmit = transmit;
//JLOG_INFO("%s session %p", msg, session);
found = true;
}
has_watchers = (has_watchers || client->transmit);
});
if (!found) {
JLOG_WARN("No session %p", session);
}
atomic_store(&_g_has_watchers, has_watchers);
UNLOCK;
}
#undef IF_DISABLED
static void _plugin_setup_media(janus_plugin_session *session) { _set_transmit(session, "Unmuted", true); }
static void _plugin_hangup_media(janus_plugin_session *session) { _set_transmit(session, "Muted", false); }
static struct janus_plugin_result *_plugin_handle_message(
janus_plugin_session *session, char *transaction, json_t *msg, json_t *jsep) {
assert(transaction != NULL);
# define FREE_MSG_JSEP { \
if (msg) json_decref(msg); \
if (jsep) json_decref(jsep); \
}
if (session == NULL || msg == NULL) {
free(transaction);
FREE_MSG_JSEP;
return janus_plugin_result_new(JANUS_PLUGIN_ERROR, (msg ? "No session" : "No message"), NULL);
}
# define PUSH_ERROR(_error, _reason) { \
/*JLOG_ERROR("Message error in session %p: %s", session, _reason);*/ \
json_t *_event = json_object(); \
json_object_set_new(_event, "ustreamer", json_string("event")); \
json_object_set_new(_event, "error_code", json_integer(_error)); \
json_object_set_new(_event, "error", json_string(_reason)); \
_g_gw->push_event(session, &_plugin, transaction, _event, NULL); \
json_decref(_event); \
}
json_t *request_obj = json_object_get(msg, "request");
if (request_obj == NULL) {
PUSH_ERROR(400, "Request missing");
goto ok_wait;
}
const char *request_str = json_string_value(request_obj);
if (!request_str) {
PUSH_ERROR(400, "Request not a string");
goto ok_wait;
}
//JLOG_INFO("Message: %s", request_str);
# define PUSH_STATUS(_status, _jsep) { \
json_t *_event = json_object(); \
json_object_set_new(_event, "ustreamer", json_string("event")); \
json_t *_result = json_object(); \
json_object_set_new(_result, "status", json_string(_status)); \
json_object_set_new(_event, "result", _result); \
_g_gw->push_event(session, &_plugin, transaction, _event, _jsep); \
json_decref(_event); \
}
if (!strcmp(request_str, "start")) {
PUSH_STATUS("started", NULL);
} else if (!strcmp(request_str, "stop")) {
PUSH_STATUS("stopped", NULL);
} else if (!strcmp(request_str, "watch")) {
char *sdp = rtp_make_sdp(_g_rtp);
if (sdp == NULL) {
PUSH_ERROR(503, "Haven't received SPS/PPS from memsink yet");
goto ok_wait;
}
//JLOG_INFO("SDP generated:\n%s", sdp);
json_t *offer_jsep = json_pack("{ssss}", "type", "offer", "sdp", sdp);
free(sdp);
PUSH_STATUS("started", offer_jsep);
json_decref(offer_jsep);
} else {
PUSH_ERROR(405, "Not implemented");
}
ok_wait:
FREE_MSG_JSEP;
return janus_plugin_result_new(JANUS_PLUGIN_OK_WAIT, NULL, NULL);
# undef PUSH_ERROR
# undef FREE_MSG_JSEP
}
static int _plugin_get_api_compatibility(void) { return JANUS_PLUGIN_API_VERSION; }
static int _plugin_get_version(void) { return VERSION_U; }
static const char *_plugin_get_version_string(void) { return VERSION; }
static const char *_plugin_get_description(void) { return "Pi-KVM uStreamer Janus plugin for H.264 video"; }
static const char *_plugin_get_name(void) { return "ustreamer"; }
static const char *_plugin_get_author(void) { return "Maxim Devaev <mdevaev@gmail.com>"; }
static const char *_plugin_get_package(void) { return "janus.plugin.ustreamer"; }
#undef STOP
#undef READY
#undef UNLOCK
#undef LOCK
#undef JLOG_PERROR
#undef JLOG_ERROR
#undef JLOG_WARN
#undef JLOG_INFO

228
janus/src/rtp.c Normal file
View File

@@ -0,0 +1,228 @@
/*****************************************************************************
# #
# uStreamer - Lightweight and fast MJPG-HTTP streamer. #
# #
# This source file is partially based on this code: #
# - https://github.com/catid/kvm/blob/master/kvm_pipeline/src #
# #
# 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 "rtp.h"
#define PAYLOAD 96 // Payload type
#define PRE 3 // Annex B prefix length
void _rtp_process_nalu(rtp_s *rtp, const uint8_t *data, size_t size, uint32_t pts, bool marked, rtp_callback_f callback);
static void _rtp_write_header(rtp_s *rtp, uint32_t pts, bool marked);
static ssize_t _find_annexb(const uint8_t *data, size_t size);
rtp_s *rtp_init(void) {
rtp_s *rtp;
A_CALLOC(rtp, 1);
rtp->ssrc = triple_u32(get_now_monotonic_u64());
rtp->sps = frame_init();
rtp->pps = frame_init();
A_MUTEX_INIT(&rtp->mutex);
return rtp;
}
void rtp_destroy(rtp_s *rtp) {
A_MUTEX_DESTROY(&rtp->mutex);
frame_destroy(rtp->pps);
frame_destroy(rtp->sps);
free(rtp);
}
char *rtp_make_sdp(rtp_s *rtp) {
A_MUTEX_LOCK(&rtp->mutex);
if (rtp->sps->used == 0 || rtp->pps->used == 0) {
A_MUTEX_UNLOCK(&rtp->mutex);
return NULL;
}
char *sps = NULL;
char *pps = NULL;
base64_encode(rtp->sps->data, rtp->sps->used, &sps, NULL);
base64_encode(rtp->pps->data, rtp->pps->used, &pps, NULL);
A_MUTEX_UNLOCK(&rtp->mutex);
// https://tools.ietf.org/html/rfc6184
// https://github.com/meetecho/janus-gateway/issues/2443
char *sdp;
A_ASPRINTF(sdp,
"v=0" RN
"o=- %" PRIu64 " 1 IN IP4 127.0.0.1" RN
"s=Pi-KVM uStreamer" RN
"t=0 0" RN
"m=video 1 RTP/SAVPF %d" RN
"c=IN IP4 0.0.0.0" RN
"a=rtpmap:%d H264/90000" RN
"a=fmtp:%d profile-level-id=42E01F" RN
"a=fmtp:%d packetization-mode=1" RN
"a=fmtp:%d sprop-sps=%s" RN
"a=fmtp:%d sprop-pps=%s" RN
"a=rtcp-fb:%d nack" RN
"a=rtcp-fb:%d nack pli" RN
"a=rtcp-fb:%d goog-remb" RN
"a=sendonly" RN,
get_now_id() >> 1, PAYLOAD, PAYLOAD, PAYLOAD, PAYLOAD,
PAYLOAD, sps,
PAYLOAD, pps,
PAYLOAD, PAYLOAD, PAYLOAD
);
free(sps);
free(pps);
return sdp;
}
void rtp_wrap_h264(rtp_s *rtp, const frame_s *frame, rtp_callback_f callback) {
// There is a complicated logic here but everything works as it should:
// - https://github.com/pikvm/ustreamer/issues/115#issuecomment-893071775
assert(frame->format == V4L2_PIX_FMT_H264);
const uint32_t pts = get_now_monotonic_u64() * 9 / 100; // PTS units are in 90 kHz
ssize_t last_offset = -PRE;
while (true) { // Find and iterate by nalus
const size_t next_start = last_offset + PRE;
ssize_t offset = _find_annexb(frame->data + next_start, frame->used - next_start);
if (offset < 0) {
break;
}
offset += next_start;
if (last_offset >= 0) {
const uint8_t *data = frame->data + last_offset + PRE;
size_t size = offset - last_offset - PRE;
if (data[size - 1] == 0) { // Check for extra 00
--size;
}
_rtp_process_nalu(rtp, data, size, pts, false, callback);
}
last_offset = offset;
}
if (last_offset >= 0) {
const uint8_t *data = frame->data + last_offset + PRE;
size_t size = frame->used - last_offset - PRE;
_rtp_process_nalu(rtp, data, size, pts, true, callback);
}
}
void _rtp_process_nalu(rtp_s *rtp, const uint8_t *data, size_t size, uint32_t pts, bool marked, rtp_callback_f callback) {
const unsigned ref_idc = (data[0] >> 5) & 3;
const unsigned type = data[0] & 0x1F;
frame_s *ps = NULL;
switch (type) {
case 7: ps = rtp->sps; break;
case 8: ps = rtp->pps; break;
}
if (ps) {
A_MUTEX_LOCK(&rtp->mutex);
frame_set_data(ps, data, size);
A_MUTEX_UNLOCK(&rtp->mutex);
}
# define HEADER_SIZE 12
if (size + HEADER_SIZE <= RTP_DATAGRAM_SIZE) {
_rtp_write_header(rtp, pts, marked);
memcpy(rtp->datagram + HEADER_SIZE, data, size);
callback(rtp->datagram, size + HEADER_SIZE);
return;
}
const size_t fu_overhead = HEADER_SIZE + 2; // FU-A overhead
const uint8_t *src = data + 1;
ssize_t remaining = size - 1;
bool first = true;
while (remaining > 0) {
ssize_t frag_size = RTP_DATAGRAM_SIZE - fu_overhead;
const bool last = (remaining <= frag_size);
if (last) {
frag_size = remaining;
}
_rtp_write_header(rtp, pts, (marked && last));
rtp->datagram[HEADER_SIZE] = 28 | (ref_idc << 5);
uint8_t fu = type;
if (first) {
fu |= 0x80;
}
if (last) {
fu |= 0x40;
}
rtp->datagram[HEADER_SIZE + 1] = fu;
memcpy(rtp->datagram + fu_overhead, src, frag_size);
callback(rtp->datagram, fu_overhead + frag_size);
src += frag_size;
remaining -= frag_size;
first = false;
}
# undef HEADER_SIZE
}
static void _rtp_write_header(rtp_s *rtp, uint32_t pts, bool marked) {
uint32_t word0 = 0x80000000;
if (marked) {
word0 |= 1 << 23;
}
word0 |= (PAYLOAD & 0x7F) << 16;
word0 |= rtp->seq;
++rtp->seq;
# define WRITE_BE_U32(_offset, _value) *((uint32_t *)(rtp->datagram + _offset)) = __builtin_bswap32(_value)
WRITE_BE_U32(0, word0);
WRITE_BE_U32(4, pts);
WRITE_BE_U32(8, rtp->ssrc);
# undef WRITE_BE_U32
}
static ssize_t _find_annexb(const uint8_t *data, size_t size) {
// Parses buffer for 00 00 01 start codes
if (size >= PRE) {
for (size_t index = 0; index <= size - PRE; ++index) {
if (data[index] == 0 && data[index + 1] == 0 && data[index + 2] == 1) {
return index;
}
}
}
return -1;
}
#undef PRE
#undef PAYLOAD

69
janus/src/rtp.h Normal file
View File

@@ -0,0 +1,69 @@
/*****************************************************************************
# #
# uStreamer - Lightweight and fast MJPG-HTTP streamer. #
# #
# This source file is partially based on this code: #
# - https://github.com/catid/kvm/blob/master/kvm_pipeline/src #
# #
# 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 <stdlib.h>
#include <stdbool.h>
#include <stdint.h>
#include <inttypes.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <linux/videodev2.h>
#include <pthread.h>
#include "tools.h"
#include "threading.h"
#include "frame.h"
#include "base64.h"
// https://stackoverflow.com/questions/47635545/why-webrtc-chose-rtp-max-packet-size-to-1200-bytes
#define RTP_DATAGRAM_SIZE 1200
typedef struct {
uint32_t ssrc;
uint16_t seq;
uint8_t datagram[RTP_DATAGRAM_SIZE];
frame_s *sps; // Actually not a frame, just a bytes storage
frame_s *pps;
pthread_mutex_t mutex;
} rtp_s;
typedef void (*rtp_callback_f)(const uint8_t *datagram, size_t size);
rtp_s *rtp_init(void);
void rtp_destroy(rtp_s *rtp);
char *rtp_make_sdp(rtp_s *rtp);
void rtp_wrap_h264(rtp_s *rtp, const frame_s *frame, rtp_callback_f callback);

1
janus/src/threading.h Symbolic link
View File

@@ -0,0 +1 @@
../../src/libs/threading.h

1
janus/src/tools.h Symbolic link
View File

@@ -0,0 +1 @@
../../src/libs/tools.h

View File

@@ -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

4
linters/cppcheck.h Normal file
View File

@@ -0,0 +1,4 @@
#define CHAR_BIT 8
#define WITH_OMX
#define WITH_GPIO
#define JANUS_PLUGIN_INIT(...) { __VA_ARGS__ }

View File

@@ -30,6 +30,7 @@ disable =
too-many-ancestors,
no-else-return,
len-as-condition,
unspecified-encoding,
[REPORTS]
msg-template = {symbol} -- {path}:{line}({obj}): {msg}

View File

@@ -3,7 +3,7 @@ envlist = cppcheck, flake8, pylint, mypy, vulture, htmlhint
skipsdist = true
[testenv]
basepython = python3.9
basepython = python3.10
changedir = /src
[testenv:cppcheck]
@@ -17,33 +17,32 @@ commands = cppcheck \
--suppress=assignmentInAssert \
--suppress=variableScope \
--inline-suppr \
-DCHAR_BIT=8 \
-DWITH_OMX \
-DWITH_GPIO \
src
--library=python \
--include=linters/cppcheck.h \
src python/ustreamer.c janus/rtp.h janus/rtp.c janus/plugin.c
[testenv:flake8]
whitelist_externals = bash
commands = bash -c 'flake8 --config=linters/flake8.ini tools/*.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'
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'
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'
commands = bash -c 'vulture tools/*.py python/*.py'
deps =
vulture

View File

@@ -1,6 +1,6 @@
.\" Manpage for ustreamer-dump.
.\" Open an issue or pull request to https://github.com/pikvm/ustreamer to correct errors or typos
.TH USTREAMER-DUMP 1 "version 3.5" "January 2021"
.TH USTREAMER-DUMP 1 "version 4.11" "January 2021"
.SH NAME
ustreamer-dump \- Dump uStreamer's memory sink to file
@@ -38,6 +38,12 @@ Filename to dump output to. Use '-' for stdout. Default: just consume the sink.
.TP
.BR \-j ", " \-\-output-json
Format output as JSON. Required option --output. Default: disabled.
.TP
.BR \-c ", " \-\-count\ \fIN
Limit the number of frames. Default: 0 (infinite).
.TP
.BR \-i ", "\-\-interval\ \fIsec
Delay between reading frames (float). Default: 0.
.SS "Logging options"
.TP

View File

@@ -1,6 +1,6 @@
.\" Manpage for ustreamer.
.\" Open an issue or pull request to https://github.com/pikvm/ustreamer to correct errors or typos
.TH USTREAMER 1 "version 3.5" "November 2020"
.TH USTREAMER 1 "version 4.11" "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
@@ -210,6 +210,9 @@ Set JPEG sink permissions (like 777). Default: 660.
.BR \-\-sink\-rm
Remove shared memory on stop. Default: disabled.
.TP
.BR \-\-sink\-client\-ttl\ \fIsec
Client TTL. Default: 10.
.TP
.BR \-\-sink\-timeout\ \fIsec
Timeout for lock. Default: 1.
@@ -226,8 +229,17 @@ Set H264 sink permissions (like 777). Default: 660.
.BR \-\-h264\-sink\-rm
Remove shared memory on stop. Default: disabled.
.TP
.BR \-\-h264\-sink\-client\-ttl\ \fIsec
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

View File

@@ -3,36 +3,51 @@
pkgname=ustreamer
pkgver=3.5
pkgver=4.11
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/bin/python3 ]; then
_options="$_options WITH_PYTHON=1"
depends+=(python)
makedepends+=(python-setuptools)
fi
if [ -e /opt/vc/include/IL/OMX_Core.h ]; then
depends+=(raspberrypi-firmware)
makedepends+=(raspberrypi-firmware)
_options="$_options WITH_OMX=1"
fi
if [ -e /usr/include/janus/plugins/plugin.h ];then
depends+=(janus-gateway-pikvm)
makedepends+=(janus-gateway-pikvm)
_options="$_options WITH_JANUS=1"
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
local _options="WITH_GPIO=1"
[ -e /opt/vc/include/IL/OMX_Core.h ] && _options="$_options WITH_OMX=1"
# 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
}

View File

@@ -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/*

View File

@@ -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/*

View File

@@ -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/*

View File

@@ -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}"

View File

@@ -6,7 +6,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=ustreamer
PKG_VERSION:=3.5
PKG_VERSION:=4.11
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

20
python/Makefile Normal file
View File

@@ -0,0 +1,20 @@
-include ../config.mk
DESTDIR ?=
PREFIX ?= /usr/local
PY ?= python3
# =====
all:
$(info == PY_BUILD ustreamer-*.so)
@ $(PY) setup.py build
install:
$(PY) setup.py install --prefix=$(PREFIX) --root=$(if $(DESTDIR),$(DESTDIR),/)
clean:
rm -rf build

25
python/setup.py Normal file
View File

@@ -0,0 +1,25 @@
import os
from distutils.core import Extension
from distutils.core import setup
# =====
if __name__ == "__main__":
setup(
name="ustreamer",
version="4.11",
description="uStreamer tools",
author="Maxim Devaev",
author_email="mdevaev@gmail.com",
url="https://github.com/pikvm/ustreamer",
ext_modules=[
Extension(
"ustreamer",
libraries=["rt", "m", "pthread"],
undef_macros=["NDEBUG"],
sources=["src/" + name for name in os.listdir("src") if name.endswith(".c")],
depends=["src/" + name for name in os.listdir("src") if name.endswith(".h")],
),
],
)

1
python/src/frame.c Symbolic link
View File

@@ -0,0 +1 @@
../../src/libs/frame.c

1
python/src/frame.h Symbolic link
View File

@@ -0,0 +1 @@
../../src/libs/frame.h

1
python/src/memsinksh.h Symbolic link
View File

@@ -0,0 +1 @@
../../src/libs/memsinksh.h

1
python/src/tools.h Symbolic link
View File

@@ -0,0 +1 @@
../../src/libs/tools.h

318
python/src/ustreamer.c Normal file
View File

@@ -0,0 +1,318 @@
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <Python.h>
#include "tools.h"
#include "frame.h"
#include "memsinksh.h"
typedef struct {
PyObject_HEAD
char *obj;
double lock_timeout;
double wait_timeout;
double drop_same_frames;
int fd;
memsink_shared_s *mem;
uint64_t frame_id;
long double frame_ts;
frame_s *frame;
} MemsinkObject;
#define MEM(_next) self->mem->_next
#define FRAME(_next) self->frame->_next
static void MemsinkObject_destroy_internals(MemsinkObject *self) {
if (self->mem != NULL) {
memsink_shared_unmap(self->mem);
self->mem = NULL;
}
if (self->fd > 0) {
close(self->fd);
self->fd = -1;
}
if (self->frame) {
frame_destroy(self->frame);
self->frame = NULL;
}
}
static int MemsinkObject_init(MemsinkObject *self, PyObject *args, PyObject *kwargs) {
self->lock_timeout = 1;
self->wait_timeout = 1;
static char *kws[] = {"obj", "lock_timeout", "wait_timeout", "drop_same_frames", NULL};
if (!PyArg_ParseTupleAndKeywords(
args, kwargs, "s|ddd", kws,
&self->obj, &self->lock_timeout, &self->wait_timeout, &self->drop_same_frames)) {
return -1;
}
# define SET_DOUBLE(_field, _cond) { \
if (!(self->_field _cond)) { \
PyErr_SetString(PyExc_ValueError, #_field " must be " #_cond); \
return -1; \
} \
}
SET_DOUBLE(lock_timeout, > 0);
SET_DOUBLE(wait_timeout, > 0);
SET_DOUBLE(drop_same_frames, >= 0);
# undef SET_DOUBLE
self->frame = frame_init();
if ((self->fd = shm_open(self->obj, O_RDWR, 0)) == -1) {
PyErr_SetFromErrno(PyExc_OSError);
goto error;
}
if ((self->mem = memsink_shared_map(self->fd)) == NULL) {
PyErr_SetFromErrno(PyExc_OSError);
goto error;
}
return 0;
error:
MemsinkObject_destroy_internals(self);
return -1;
}
static PyObject *MemsinkObject_repr(MemsinkObject *self) {
char repr[1024];
snprintf(repr, 1023, "<Memsink(%s)>", self->obj);
return Py_BuildValue("s", repr);
}
static void MemsinkObject_dealloc(MemsinkObject *self) {
MemsinkObject_destroy_internals(self);
PyObject_Del(self);
}
static PyObject *MemsinkObject_close(MemsinkObject *self, PyObject *Py_UNUSED(ignored)) {
MemsinkObject_destroy_internals(self);
Py_RETURN_NONE;
}
static PyObject *MemsinkObject_enter(MemsinkObject *self, PyObject *Py_UNUSED(ignored)) {
Py_INCREF(self);
return (PyObject *)self;
}
static PyObject *MemsinkObject_exit(MemsinkObject *self, PyObject *Py_UNUSED(ignored)) {
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 && MEM(id) != self->frame_id) {
if (self->drop_same_frames > 0) {
if (
FRAME_COMPARE_META_USED_NOTS(self->mem, self->frame)
&& (self->frame_ts + self->drop_same_frames > now)
&& !memcmp(FRAME(data), MEM(data), MEM(used))
) {
self->frame_id = MEM(id);
goto drop;
}
}
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;
}
switch (wait_frame(self)) {
case 0: break;
case -2: Py_RETURN_NONE;
default: return NULL;
}
frame_set_data(self->frame, MEM(data), MEM(used));
FRAME_COPY_META(self->mem, self->frame);
self->frame_id = MEM(id);
self->frame_ts = get_now_monotonic();
MEM(last_client_ts) = self->frame_ts;
if (flock(self->fd, LOCK_UN) < 0) {
return PyErr_SetFromErrno(PyExc_OSError);
}
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(FRAME(_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 *)FRAME(data), FRAME(used)));
# undef SET_NUMBER
# undef SET_VALUE
return dict_frame;
}
static PyObject *MemsinkObject_is_opened(MemsinkObject *self, PyObject *Py_UNUSED(ignored)) {
return PyBool_FromLong(self->mem != NULL && self->fd > 0);
}
#define FIELD_GETTER(_field, _from, _to) \
static PyObject *MemsinkObject_getter_##_field(MemsinkObject *self, void *Py_UNUSED(closure)) { \
return Py##_to##_From##_from(self->_field); \
}
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(_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
};
static PyGetSetDef MemsinkObject_getsets[] = {
# define ADD_GETTER(_field) {.name = #_field, .get = (getter)MemsinkObject_getter_##_field}
ADD_GETTER(obj),
ADD_GETTER(lock_timeout),
ADD_GETTER(wait_timeout),
ADD_GETTER(drop_same_frames),
{},
# undef ADD_GETTER
};
static PyTypeObject MemsinkType = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "ustreamer.Memsink",
.tp_basicsize = sizeof(MemsinkObject),
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_new = PyType_GenericNew,
.tp_init = (initproc)MemsinkObject_init,
.tp_dealloc = (destructor)MemsinkObject_dealloc,
.tp_repr = (reprfunc)MemsinkObject_repr,
.tp_methods = MemsinkObject_methods,
.tp_getset = MemsinkObject_getsets,
};
static PyModuleDef ustreamer_Module = {
PyModuleDef_HEAD_INIT,
.m_name = "ustreamer",
.m_size = -1,
};
PyMODINIT_FUNC PyInit_ustreamer(void) { // cppcheck-suppress unusedFunction
PyObject *module = PyModule_Create(&ustreamer_Module);
if (module == NULL) {
return NULL;
}
if (PyType_Ready(&MemsinkType) < 0) {
return NULL;
}
Py_INCREF(&MemsinkType);
if (PyModule_AddObject(module, "Memsink", (PyObject *)&MemsinkType) < 0) {
return NULL;
}
return module;
}
#undef FRAME
#undef MEM

113
src/Makefile Normal file
View File

@@ -0,0 +1,113 @@
DESTDIR ?=
PREFIX ?= /usr/local
CC ?= gcc
CFLAGS ?= -O3
LDFLAGS ?=
RPI_VC_HEADERS ?= /opt/vc/include
RPI_VC_LIBS ?= /opt/vc/lib
# =====
_USTR = ustreamer.bin
_DUMP = ustreamer-dump.bin
_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
_USTR_SRCS = $(shell ls \
libs/*.c \
ustreamer/*.c \
ustreamer/http/*.c \
ustreamer/data/*.c \
ustreamer/encoders/cpu/*.c \
ustreamer/encoders/hw/*.c \
)
_DUMP_LIBS = $(_COMMON_LIBS)
_DUMP_SRCS = $(shell ls \
libs/*.c \
dump/*.c \
)
_BUILD = build
define optbool
$(filter $(shell echo $(1) | tr A-Z a-z), yes on 1)
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)
_USTR_SRCS += $(shell ls \
ustreamer/encoders/omx/*.c \
ustreamer/h264/*.c \
)
endif
ifneq ($(call optbool,$(WITH_GPIO)),)
_USTR_LIBS += -lgpiod
override _CFLAGS += -DWITH_GPIO
_USTR_SRCS += $(shell ls ustreamer/gpio/*.c)
endif
WITH_PTHREAD_NP ?= 1
ifneq ($(call optbool,$(WITH_PTHREAD_NP)),)
override _CFLAGS += -DWITH_PTHREAD_NP
endif
WITH_SETPROCTITLE ?= 1
ifneq ($(call optbool,$(WITH_SETPROCTITLE)),)
ifeq ($(shell uname -s | tr A-Z a-z),linux)
_USTR_LIBS += -lbsd
endif
override _CFLAGS += -DWITH_SETPROCTITLE
endif
# =====
all: $(_USTR) $(_DUMP)
install: all
mkdir -p $(DESTDIR)$(PREFIX)/bin
install -m755 $(_USTR) $(DESTDIR)$(PREFIX)/bin/$(subst .bin,,$(_USTR))
install -m755 $(_DUMP) $(DESTDIR)$(PREFIX)/bin/$(subst .bin,,$(_DUMP))
install-strip: install
strip $(DESTDIR)$(PREFIX)/bin/$(subst .bin,,$(_USTR))
strip $(DESTDIR)$(PREFIX)/bin/$(subst .bin,,$(_DUMP))
$(_USTR): $(_USTR_SRCS:%.c=$(_BUILD)/%.o)
$(info == LD $@)
@ $(CC) $^ -o $@ $(_LDFLAGS) $(_USTR_LIBS)
$(_DUMP): $(_DUMP_SRCS:%.c=$(_BUILD)/%.o)
$(info == LD $@)
@ $(CC) $^ -o $@ $(_LDFLAGS) $(_DUMP_LIBS)
$(_BUILD)/%.o: %.c
$(info -- CC $<)
@ mkdir -p $(dir $@) || true
@ $(CC) $< -o $@ $(_CFLAGS)
clean:
rm -rf $(_USTR) $(_DUMP) $(_BUILD)
_OBJS = $(_USTR_SRCS:%.c=$(_BUILD)/%.o) $(_DUMP_SRCS:%.c=$(_BUILD)/%.o)
-include $(_OBJS:%.o=%.d)

79
src/dump/file.c Normal file
View File

@@ -0,0 +1,79 @@
/*****************************************************************************
# #
# 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 "file.h"
output_file_s *output_file_init(const char *path, bool json) {
output_file_s *output;
A_CALLOC(output, 1);
if (!strcmp(path, "-")) {
LOG_INFO("Using output: <stdout>");
output->fp = stdout;
} else {
LOG_INFO("Using output: %s", path);
if ((output->fp = fopen(path, "wb")) == NULL) {
LOG_PERROR("Can't open output file");
goto error;
}
}
output->json = json;
return output;
error:
output_file_destroy(output);
return NULL;
}
void output_file_write(void *v_output, const frame_s *frame) {
output_file_s *output = (output_file_s *)v_output;
if (output->json) {
base64_encode(frame->data, frame->used, &output->base64_data, &output->base64_allocated);
fprintf(output->fp,
"{\"size\": %zu, \"width\": %u, \"height\": %u,"
" \"format\": %u, \"stride\": %u, \"online\": %u,"
" \"grab_ts\": %.3Lf, \"encode_begin_ts\": %.3Lf, \"encode_end_ts\": %.3Lf,"
" \"data\": \"%s\"}\n",
frame->used, frame->width, frame->height,
frame->format, frame->stride, frame->online,
frame->grab_ts, frame->encode_begin_ts, frame->encode_end_ts,
output->base64_data);
} else {
fwrite(frame->data, 1, frame->used, output->fp);
}
fflush(output->fp);
}
void output_file_destroy(void *v_output) {
output_file_s *output = (output_file_s *)v_output;
if (output->base64_data) {
free(output->base64_data);
}
if (output->fp && output->fp != stdout) {
if (fclose(output->fp) < 0) {
LOG_PERROR("Can't close output file");
}
}
free(output);
}

49
src/dump/file.h Normal file
View File

@@ -0,0 +1,49 @@
/*****************************************************************************
# #
# 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 <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <sys/types.h>
#include "../libs/tools.h"
#include "../libs/logging.h"
#include "../libs/frame.h"
#include "../libs/base64.h"
typedef struct {
const char *path;
bool json;
FILE *fp;
char *base64_data;
size_t base64_allocated;
} output_file_s;
output_file_s *output_file_init(const char *path, bool json);
void output_file_write(void *v_output, const frame_s *frame);
void output_file_destroy(void *v_output);

View File

@@ -23,7 +23,10 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <signal.h>
#include <limits.h>
#include <float.h>
#include <getopt.h>
#include <errno.h>
#include <assert.h>
@@ -33,7 +36,9 @@
#include "../libs/logging.h"
#include "../libs/frame.h"
#include "../libs/memsink.h"
#include "../libs/base64.h"
#include "../libs/options.h"
#include "file.h"
enum _OPT_VALUES {
@@ -41,6 +46,8 @@ enum _OPT_VALUES {
_O_SINK_TIMEOUT = 't',
_O_OUTPUT = 'o',
_O_OUTPUT_JSON = 'j',
_O_COUNT = 'c',
_O_INTERVAL = 'i',
_O_HELP = 'h',
_O_VERSION = 'v',
@@ -58,6 +65,8 @@ static const struct option _LONG_OPTS[] = {
{"sink-timeout", required_argument, NULL, _O_SINK_TIMEOUT},
{"output", required_argument, NULL, _O_OUTPUT},
{"output-json", no_argument, NULL, _O_OUTPUT_JSON},
{"count", required_argument, NULL, _O_COUNT},
{"interval", required_argument, NULL, _O_INTERVAL},
{"log-level", required_argument, NULL, _O_LOG_LEVEL},
{"perf", no_argument, NULL, _O_PERF},
@@ -73,13 +82,23 @@ static const struct option _LONG_OPTS[] = {
};
volatile bool stop = false;
volatile bool global_stop = false;
typedef struct {
void *v_output;
void (*write)(void *v_output, const frame_s *frame);
void (*destroy)(void *v_output);
} _output_context_s;
static void _signal_handler(int signum);
static void _install_signal_handlers(void);
static int _dump_sink(const char *sink_name, unsigned sink_timeout, const char *output_path, bool output_json);
static int _dump_sink(
const char *sink_name, unsigned sink_timeout,
long long count, long double interval,
_output_context_s *ctx);
static void _help(FILE *fp);
@@ -92,6 +111,8 @@ int main(int argc, char *argv[]) {
unsigned sink_timeout = 1;
char *output_path = NULL;
bool output_json = false;
long long count = 0;
long double interval = 0;
# define OPT_SET(_dest, _value) { \
_dest = _value; \
@@ -108,28 +129,44 @@ int main(int argc, char *argv[]) {
break; \
}
for (int ch; (ch = getopt_long(argc, argv, "s:t:o:jhv", _LONG_OPTS, NULL)) >= 0;) {
# define OPT_LDOUBLE(_name, _dest, _min, _max) { \
errno = 0; char *_end = NULL; long double _tmp = strtold(optarg, &_end); \
if (errno || *_end || _tmp < _min || _tmp > _max) { \
printf("Invalid value for '%s=%s': min=%Lf, max=%Lf\n", _name, optarg, (long double)_min, (long double)_max); \
return 1; \
} \
_dest = _tmp; \
break; \
}
char short_opts[128];
build_short_options(_LONG_OPTS, short_opts, 128);
for (int ch; (ch = getopt_long(argc, argv, short_opts, _LONG_OPTS, NULL)) >= 0;) {
switch (ch) {
case _O_SINK: OPT_SET(sink_name, optarg);
case _O_SINK_TIMEOUT: OPT_NUMBER("--sink-timeout", sink_timeout, 1, 60, 0);
case _O_OUTPUT: OPT_SET(output_path, optarg);
case _O_OUTPUT_JSON: OPT_SET(output_json, true);
case _O_COUNT: OPT_NUMBER("--count", count, 0, LLONG_MAX, 0);
case _O_INTERVAL: OPT_LDOUBLE("--interval", interval, 0, 60);
case _O_LOG_LEVEL: OPT_NUMBER("--log-level", log_level, LOG_LEVEL_INFO, LOG_LEVEL_DEBUG, 0);
case _O_PERF: OPT_SET(log_level, LOG_LEVEL_PERF);
case _O_VERBOSE: OPT_SET(log_level, LOG_LEVEL_VERBOSE);
case _O_DEBUG: OPT_SET(log_level, LOG_LEVEL_DEBUG);
case _O_FORCE_LOG_COLORS: OPT_SET(log_colored, true);
case _O_NO_LOG_COLORS: OPT_SET(log_colored, false);
case _O_LOG_LEVEL: OPT_NUMBER("--log-level", us_log_level, LOG_LEVEL_INFO, LOG_LEVEL_DEBUG, 0);
case _O_PERF: OPT_SET(us_log_level, LOG_LEVEL_PERF);
case _O_VERBOSE: OPT_SET(us_log_level, LOG_LEVEL_VERBOSE);
case _O_DEBUG: OPT_SET(us_log_level, LOG_LEVEL_DEBUG);
case _O_FORCE_LOG_COLORS: OPT_SET(us_log_colored, true);
case _O_NO_LOG_COLORS: OPT_SET(us_log_colored, false);
case _O_HELP: _help(stdout); return 0;
case _O_VERSION: puts(VERSION); return 0;
case 0: break;
default: _help(stderr); return 1;
default: return 1;
}
}
# undef OPT_LDOUBLE
# undef OPT_NUMBER
# undef OPT_SET
@@ -138,8 +175,23 @@ int main(int argc, char *argv[]) {
return 1;
}
_output_context_s ctx;
MEMSET_ZERO(ctx);
if (output_path && output_path[0] != '\0') {
if ((ctx.v_output = (void *)output_file_init(output_path, output_json)) == NULL) {
return 1;
}
ctx.write = output_file_write;
ctx.destroy = output_file_destroy;
}
_install_signal_handlers();
return abs(_dump_sink(sink_name, sink_timeout, output_path, output_json));
int retval = abs(_dump_sink(sink_name, sink_timeout, count, interval, &ctx));
if (ctx.v_output && ctx.destroy) {
ctx.destroy(ctx.v_output);
}
return retval;
}
@@ -150,7 +202,7 @@ static void _signal_handler(int signum) {
case SIGPIPE: LOG_INFO_NOLOCK("===== Stopping by SIGPIPE ====="); break;
default: LOG_INFO_NOLOCK("===== Stopping by %d =====", signum); break;
}
stop = true;
global_stop = true;
}
static void _install_signal_handlers(void) {
@@ -173,27 +225,21 @@ static void _install_signal_handlers(void) {
assert(!sigaction(SIGPIPE, &sig_act, NULL));
}
static int _dump_sink(const char *sink_name, unsigned sink_timeout, const char *output_path, bool output_json) {
frame_s *frame = frame_init("input");
memsink_s *sink = NULL;
FILE *output_fp = NULL;
char *base64_data = NULL;
size_t base64_allocated = 0;
static int _dump_sink(
const char *sink_name, unsigned sink_timeout,
long long count, long double interval,
_output_context_s *ctx) {
if (output_path && output_path[0] != '\0') {
if (!strcmp(output_path, "-")) {
LOG_INFO("Using output: <stdout>");
output_fp = stdout;
} else {
LOG_INFO("Using output: %s", output_path);
if ((output_fp = fopen(output_path, "wb")) == NULL) {
LOG_PERROR("Can't open output file");
goto error;
}
}
if (count == 0) {
count = -1;
}
if ((sink = memsink_init("input", sink_name, false, 0, false, sink_timeout)) == NULL) {
useconds_t interval_us = interval * 1000000;
frame_s *frame = frame_init();
memsink_s *sink = NULL;
if ((sink = memsink_init("input", sink_name, false, 0, false, 0, sink_timeout)) == NULL) {
goto error;
}
@@ -201,18 +247,21 @@ static int _dump_sink(const char *sink_name, unsigned sink_timeout, const char *
unsigned fps_accum = 0;
long long fps_second = 0;
while (!stop) {
long double last_ts = 0;
while (!global_stop) {
int error = memsink_client_get(sink, frame);
if (error == 0) {
const long double now = get_now_monotonic();
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);
@@ -225,24 +274,23 @@ static int _dump_sink(const char *sink_name, unsigned sink_timeout, const char *
}
fps_accum += 1;
if (output_fp) {
if (output_json) {
base64_encode(frame->data, frame->used, &base64_data, &base64_allocated);
fprintf(output_fp,
"{\"size\": %zu, \"width\": %u, \"height\": %u,"
" \"format\": %u, \"stride\": %u, \"online\": %u,"
" \"grab_ts\": %.3Lf, \"encode_begin_ts\": %.3Lf, \"encode_end_ts\": %.3Lf,"
" \"data\": \"%s\"}\n",
frame->used, frame->width, frame->height,
frame->format, frame->stride, frame->online,
frame->grab_ts, frame->encode_begin_ts, frame->encode_end_ts,
base64_data);
} else {
fwrite(frame->data, 1, frame->used, output_fp);
}
fflush(output_fp);
if (ctx->v_output) {
ctx->write(ctx->v_output, frame);
}
} else if (error != -2) {
if (count >= 0) {
--count;
if (count <= 0) {
break;
}
}
if (interval_us > 0) {
usleep(interval_us);
}
} else if (error == -2) {
usleep(1000);
} else {
goto error;
}
}
@@ -254,14 +302,6 @@ static int _dump_sink(const char *sink_name, unsigned sink_timeout, const char *
retval = -1;
ok:
if (base64_data) {
free(base64_data);
}
if (output_fp && output_fp != stdout) {
if (fclose(output_fp) < 0) {
LOG_PERROR("Can't close output file");
}
}
if (sink) {
memsink_destroy(sink);
}
@@ -287,12 +327,14 @@ static void _help(FILE *fp) {
SAY(" -t|--sink-timeout <sec> ─ Timeout for the upcoming frame. Default: 1.\n");
SAY(" -o|--output <filename> ─── Filename to dump output to. Use '-' for stdout. Default: just consume the sink.\n");
SAY(" -j|--output-json ──────── Format output as JSON. Required option --output. Default: disabled.\n");
SAY(" -c|--count <N> ───────── Limit the number of frames. Default: 0 (infinite).\n");
SAY(" -i|--interval <sec> ───── Delay between reading frames (float). Default: 0.\n");
SAY("Logging options:");
SAY("════════════════");
SAY(" --log-level <N> ──── Verbosity level of messages from 0 (info) to 3 (debug).");
SAY(" Enabling debugging messages can slow down the program.");
SAY(" Available levels: 0 (info), 1 (performance), 2 (verbose), 3 (debug).");
SAY(" Default: %d.\n", log_level);
SAY(" Default: %d.\n", us_log_level);
SAY(" --perf ───────────── Enable performance messages (same as --log-level=1). Default: disabled.\n");
SAY(" --verbose ────────── Enable verbose messages and lower (same as --log-level=2). Default: disabled.\n");
SAY(" --debug ──────────── Enable debug messages and lower (same as --log-level=3). Default: disabled.\n");

View File

@@ -22,6 +22,11 @@
#pragma once
#ifndef VERSION
# define VERSION "3.5"
#endif
#define VERSION_MAJOR 4
#define VERSION_MINOR 11
#define MAKE_VERSION2(_major, _minor) #_major "." #_minor
#define MAKE_VERSION1(_major, _minor) MAKE_VERSION2(_major, _minor)
#define VERSION MAKE_VERSION1(VERSION_MAJOR, VERSION_MINOR)
#define VERSION_U ((unsigned)(VERSION_MAJOR * 1000 + VERSION_MINOR))

View File

@@ -23,17 +23,14 @@
#include "frame.h"
frame_s *frame_init(const char *name) {
frame_s *frame_init(void) {
frame_s *frame;
A_CALLOC(frame, 1);
frame->name = name;
frame->managed = true;
frame_realloc_data(frame, 512 * 1024);
return frame;
}
void frame_destroy(frame_s *frame) {
assert(frame->managed);
if (frame->data) {
free(frame->data);
}
@@ -41,66 +38,36 @@ void frame_destroy(frame_s *frame) {
}
void frame_realloc_data(frame_s *frame, size_t size) {
assert(frame->managed);
if (frame->allocated < size) {
LOG_DEBUG("Increasing frame buffer '%s': %zu -> %zu (+%zu)",
frame->name, frame->allocated, size, size - frame->allocated);
A_REALLOC(frame->data, size);
frame->allocated = size;
}
}
void frame_set_data(frame_s *frame, const uint8_t *data, size_t size) {
assert(frame->managed);
frame_realloc_data(frame, size);
memcpy(frame->data, data, size);
frame->used = size;
}
void frame_append_data(frame_s *frame, const uint8_t *data, size_t size) {
assert(frame->managed);
size_t new_used = frame->used + size;
frame_realloc_data(frame, new_used);
memcpy(frame->data + frame->used, data, size);
frame->used = new_used;
}
#define COPY(_field) dest->_field = src->_field
void frame_copy(const frame_s *src, frame_s *dest) {
assert(dest->managed);
frame_set_data(dest, src->data, src->used);
COPY(used);
frame_copy_meta(src, dest);
FRAME_COPY_META(src, dest);
}
void frame_copy_meta(const frame_s *src, frame_s *dest) {
// Don't copy the name
COPY(width);
COPY(height);
COPY(format);
COPY(stride);
COPY(online);
COPY(grab_ts);
COPY(encode_begin_ts);
COPY(encode_end_ts);
}
#undef COPY
bool frame_compare(const frame_s *a, const frame_s *b) {
# define CMP(_field) (a->_field == b->_field)
return (
a->allocated && b->allocated
&& CMP(used)
&& CMP(width)
&& CMP(height)
&& CMP(format)
&& CMP(stride)
&& CMP(online)
&& FRAME_COMPARE_META_USED_NOTS(a, b)
&& !memcmp(a->data, b->data, b->used)
);
# undef CMP
}
unsigned frame_get_padding(const frame_s *frame) {

View File

@@ -32,12 +32,9 @@
#include <linux/videodev2.h>
#include "tools.h"
#include "logging.h"
typedef struct {
const char *name;
uint8_t *data;
size_t used;
size_t allocated;
@@ -51,16 +48,42 @@ 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;
long double encode_end_ts;
bool managed;
} frame_s;
frame_s *frame_init(const char *name);
#define FRAME_COPY_META(_src, _dest) { \
_dest->width = _src->width; \
_dest->height = _src->height; \
_dest->format = _src->format; \
_dest->stride = _src->stride; \
_dest->online = _src->online; \
_dest->key = _src->key; \
_dest->grab_ts = _src->grab_ts; \
_dest->encode_begin_ts = _src->encode_begin_ts; \
_dest->encode_end_ts = _src->encode_end_ts; \
}
static inline void frame_copy_meta(const frame_s *src, frame_s *dest) {
FRAME_COPY_META(src, dest);
}
#define FRAME_COMPARE_META_USED_NOTS(_a, _b) ( \
_a->used == _b->used \
&& _a->width == _b->width \
&& _a->height == _b->height \
&& _a->format == _b->format \
&& _a->stride == _b->stride \
&& _a->online == _b->online \
&& _a->key == _b->key \
)
frame_s *frame_init(void);
void frame_destroy(frame_s *frame);
void frame_realloc_data(frame_s *frame, size_t size);
@@ -68,13 +91,12 @@ void frame_set_data(frame_s *frame, const uint8_t *data, size_t size);
void frame_append_data(frame_s *frame, const uint8_t *data, size_t size);
void frame_copy(const frame_s *src, frame_s *dest);
void frame_copy_meta(const frame_s *src, frame_s *dest);
bool frame_compare(const frame_s *a, const frame_s *b);
unsigned frame_get_padding(const frame_s *frame);
const char *fourcc_to_string(unsigned format, char *buf, size_t size);
inline bool is_jpeg(unsigned format) {
static inline bool is_jpeg(unsigned format) {
return (format == V4L2_PIX_FMT_JPEG || format == V4L2_PIX_FMT_MJPEG);
}

71
src/libs/list.h Normal file
View File

@@ -0,0 +1,71 @@
/*****************************************************************************
# #
# 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 <assert.h>
#define LIST_STRUCT(...) \
__VA_ARGS__ *prev; \
__VA_ARGS__ *next;
#define LIST_ITERATE(_first, _item, ...) { \
for (__typeof__(_first) _item = _first; _item;) { \
__typeof__(_first) _next = _item->next; \
__VA_ARGS__ \
_item = _next; \
} \
}
#define LIST_APPEND(_first, _item) { \
if (_first == NULL) { \
_first = _item; \
} else { \
__typeof__(_first) _last = _first; \
for (; _last->next; _last = _last->next); \
_item->prev = _last; \
_last->next = _item; \
} \
}
#define LIST_APPEND_C(_first, _item, _count) { \
LIST_APPEND(_first, _item); \
++(_count); \
}
#define LIST_REMOVE(_first, _item) { \
if (_item->prev == NULL) { \
_first = _item->next; \
} else { \
_item->prev->next = _item->next; \
} \
if (_item->next != NULL) { \
_item->next->prev = _item->prev; \
} \
}
#define LIST_REMOVE_C(_first, _item, _count) { \
LIST_REMOVE(_first, _item); \
assert((_count) >= 1); \
--(_count); \
}

View File

@@ -23,8 +23,8 @@
#include "logging.h"
enum log_level_t log_level;
enum log_level_t us_log_level;
bool log_colored;
bool us_log_colored;
pthread_mutex_t log_mutex;
pthread_mutex_t us_log_mutex;

View File

@@ -45,23 +45,23 @@ enum log_level_t {
};
extern enum log_level_t log_level;
extern enum log_level_t us_log_level;
extern bool log_colored;
extern bool us_log_colored;
extern pthread_mutex_t log_mutex;
extern pthread_mutex_t us_log_mutex;
#define LOGGING_INIT { \
log_level = LOG_LEVEL_INFO; \
log_colored = isatty(2); \
A_MUTEX_INIT(&log_mutex); \
us_log_level = LOG_LEVEL_INFO; \
us_log_colored = isatty(2); \
A_MUTEX_INIT(&us_log_mutex); \
}
#define LOGGING_DESTROY A_MUTEX_DESTROY(&log_mutex)
#define LOGGING_DESTROY A_MUTEX_DESTROY(&us_log_mutex)
#define LOGGING_LOCK A_MUTEX_LOCK(&log_mutex)
#define LOGGING_UNLOCK A_MUTEX_UNLOCK(&log_mutex)
#define LOGGING_LOCK A_MUTEX_LOCK(&us_log_mutex)
#define LOGGING_UNLOCK A_MUTEX_UNLOCK(&us_log_mutex)
#define COLOR_GRAY "\x1b[30;1m"
@@ -84,7 +84,7 @@ extern pthread_mutex_t log_mutex;
}
#define SEP_DEBUG(_ch) { \
if (log_level >= LOG_LEVEL_DEBUG) { \
if (us_log_level >= LOG_LEVEL_DEBUG) { \
SEP_INFO(_ch); \
} \
}
@@ -93,7 +93,7 @@ extern pthread_mutex_t log_mutex;
#define LOG_PRINTF_NOLOCK(_label_color, _label, _msg_color, _msg, ...) { \
char _tname_buf[MAX_THREAD_NAME] = {0}; \
thread_get_name(_tname_buf); \
if (log_colored) { \
if (us_log_colored) { \
fprintf(stderr, COLOR_GRAY "-- " _label_color _label COLOR_GRAY \
" [%.03Lf %9s]" " -- " COLOR_RESET _msg_color _msg COLOR_RESET, \
get_now_monotonic(), _tname_buf, ##__VA_ARGS__); \
@@ -130,43 +130,33 @@ extern pthread_mutex_t log_mutex;
}
#define LOG_PERF(_msg, ...) { \
if (log_level >= LOG_LEVEL_PERF) { \
if (us_log_level >= LOG_LEVEL_PERF) { \
LOG_PRINTF(COLOR_CYAN, "PERF ", COLOR_CYAN, _msg, ##__VA_ARGS__); \
} \
}
#define LOG_PERF_FPS(_msg, ...) { \
if (log_level >= LOG_LEVEL_PERF) { \
if (us_log_level >= LOG_LEVEL_PERF) { \
LOG_PRINTF(COLOR_YELLOW, "PERF ", COLOR_YELLOW, _msg, ##__VA_ARGS__); \
} \
}
#define LOG_VERBOSE(_msg, ...) { \
if (log_level >= LOG_LEVEL_VERBOSE) { \
if (us_log_level >= LOG_LEVEL_VERBOSE) { \
LOG_PRINTF(COLOR_BLUE, "VERB ", COLOR_BLUE, _msg, ##__VA_ARGS__); \
} \
}
#define LOG_VERBOSE_PERROR(_msg, ...) { \
if (log_level >= LOG_LEVEL_VERBOSE) { \
if (us_log_level >= LOG_LEVEL_VERBOSE) { \
char _perror_buf[1024] = {0}; \
char *_perror_ptr = errno_to_string(errno, _perror_buf, 1024); \
char *_perror_ptr = errno_to_string(errno, _perror_buf, 1023); \
LOG_PRINTF(COLOR_BLUE, "VERB ", COLOR_BLUE, _msg ": %s", ##__VA_ARGS__, _perror_ptr); \
} \
}
#define LOG_DEBUG(_msg, ...) { \
if (log_level >= LOG_LEVEL_DEBUG) { \
if (us_log_level >= LOG_LEVEL_DEBUG) { \
LOG_PRINTF(COLOR_GRAY, "DEBUG", COLOR_GRAY, _msg, ##__VA_ARGS__); \
} \
}
INLINE char *errno_to_string(int error, char *buf, size_t size) {
# if defined(__GLIBC__) && defined(_GNU_SOURCE)
return strerror_r(error, buf, size);
# else
strerror_r(error, buf, size);
return buf;
# endif
}

View File

@@ -23,23 +23,29 @@
#include "memsink.h"
static int _flock_timedwait_monotonic(int fd, long double timeout);
memsink_s *memsink_init(
const char *name, const char *obj, bool server,
mode_t mode, bool rm, unsigned client_ttl, unsigned timeout) {
memsink_s *memsink_init(const char *name, const char *obj, bool server, mode_t mode, bool rm, unsigned timeout) {
memsink_s *sink;
A_CALLOC(sink, 1);
sink->name = name;
sink->obj = obj;
sink->server = server;
sink->rm = rm;
sink->client_ttl = client_ttl;
sink->timeout = timeout;
sink->fd = -1;
sink->mem = MAP_FAILED;
atomic_init(&sink->has_clients, false);
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;
}
@@ -49,14 +55,7 @@ memsink_s *memsink_init(const char *name, const char *obj, bool server, mode_t m
goto error;
}
if ((sink->mem = mmap(
NULL,
sizeof(memsink_shared_s),
PROT_READ | PROT_WRITE,
MAP_SHARED,
sink->fd,
0
)) == MAP_FAILED) {
if ((sink->mem = memsink_shared_map(sink->fd)) == NULL) {
LOG_PERROR("%s-sink: Can't mmap shared memory", name);
goto error;
}
@@ -70,7 +69,7 @@ memsink_s *memsink_init(const char *name, const char *obj, bool server, mode_t m
void memsink_destroy(memsink_s *sink) {
if (sink->mem != MAP_FAILED) {
if (munmap(sink->mem, sizeof(memsink_shared_s)) < 0) {
if (memsink_shared_unmap(sink->mem) < 0) {
LOG_PERROR("%s-sink: Can't unmap shared memory", sink->name);
}
}
@@ -87,6 +86,38 @@ void memsink_destroy(memsink_s *sink) {
free(sink);
}
bool memsink_server_check(memsink_s *sink, const frame_s *frame) {
// Return true (the need to write to memsink) on any of these conditions:
// - EWOULDBLOCK - we have an active client;
// - Incorrect magic or version - need to first write;
// - We have some active clients by last_client_ts;
// - Frame meta differs (like size, format, but not timestamp).
assert(sink->server);
if (flock(sink->fd, LOCK_EX | LOCK_NB) < 0) {
if (errno == EWOULDBLOCK) {
atomic_store(&sink->has_clients, true);
return true;
}
LOG_PERROR("%s-sink: Can't lock memory", sink->name);
return false;
}
if (sink->mem->magic != MEMSINK_MAGIC || sink->mem->version != MEMSINK_VERSION) {
return true;
}
bool has_clients = (sink->mem->last_client_ts + sink->client_ttl > get_now_monotonic());
atomic_store(&sink->has_clients, has_clients);
if (flock(sink->fd, LOCK_UN) < 0) {
LOG_PERROR("%s-sink: Can't unlock memory", sink->name);
return false;
}
return (has_clients || !FRAME_COMPARE_META_USED_NOTS(sink->mem, frame));;
}
int memsink_server_put(memsink_s *sink, const frame_s *frame) {
assert(sink->server);
@@ -98,24 +129,19 @@ int memsink_server_put(memsink_s *sink, const frame_s *frame) {
return 0; // -2
}
if (_flock_timedwait_monotonic(sink->fd, 1) == 0) {
if (flock_timedwait_monotonic(sink->fd, 1) == 0) {
LOG_VERBOSE("%s-sink: >>>>> Exposing new frame ...", sink->name);
# define COPY(_field) sink->mem->_field = frame->_field
sink->last_id = get_now_id();
sink->mem->id = sink->last_id;
COPY(used);
COPY(width);
COPY(height);
COPY(format);
COPY(stride);
COPY(online);
COPY(grab_ts);
COPY(encode_begin_ts);
COPY(encode_end_ts);
sink->has_clients = (sink->mem->last_consumed_ts + 10 > get_now_monotonic());
memcpy(sink->mem->data, frame->data, frame->used);
# undef COPY
sink->mem->used = frame->used;
FRAME_COPY_META(frame, sink->mem);
sink->mem->magic = MEMSINK_MAGIC;
sink->mem->version = MEMSINK_VERSION;
atomic_store(&sink->has_clients, (sink->mem->last_client_ts + sink->client_ttl > get_now_monotonic()));
if (flock(sink->fd, LOCK_UN) < 0) {
LOG_PERROR("%s-sink: Can't unlock memory", sink->name);
@@ -137,7 +163,7 @@ int memsink_server_put(memsink_s *sink, const frame_s *frame) {
int memsink_client_get(memsink_s *sink, frame_s *frame) { // cppcheck-suppress unusedFunction
assert(!sink->server); // Client only
if (_flock_timedwait_monotonic(sink->fd, sink->timeout) < 0) {
if (flock_timedwait_monotonic(sink->fd, sink->timeout) < 0) {
if (errno == EWOULDBLOCK) {
return -2;
}
@@ -145,48 +171,27 @@ int memsink_client_get(memsink_s *sink, frame_s *frame) { // cppcheck-suppress u
return -1;
}
bool same = false;
if (sink->mem->id == sink->last_id) {
same = true;
} else {
# define COPY(_field) frame->_field = sink->mem->_field
sink->last_id = sink->mem->id;
COPY(width);
COPY(height);
COPY(format);
COPY(stride);
COPY(online);
COPY(grab_ts);
COPY(encode_begin_ts);
COPY(encode_end_ts);
sink->mem->last_consumed_ts = get_now_monotonic();
frame_set_data(frame, sink->mem->data, sink->mem->used);
# undef COPY
}
if (flock(sink->fd, LOCK_UN) < 0) {
LOG_PERROR("%s-sink: Can't unlock memory", sink->name);
return -1;
}
if (same) {
usleep(1000);
return -2;
}
return 0;
}
static int _flock_timedwait_monotonic(int fd, long double timeout) {
long double deadline_ts = get_now_monotonic() + timeout;
int retval = -1;
while (true) {
retval = flock(fd, LOCK_EX | LOCK_NB);
if (retval == 0 || errno != EWOULDBLOCK || get_now_monotonic() > deadline_ts) {
break;
int retval = -2; // Not updated
if (sink->mem->magic == MEMSINK_MAGIC) {
if (sink->mem->version != MEMSINK_VERSION) {
LOG_ERROR("%s-sink: Protocol version mismatch: sink=%u, required=%u",
sink->name, sink->mem->version, MEMSINK_VERSION);
retval = -1;
goto done;
}
usleep(1000);
if (sink->mem->id != sink->last_id) { // When updated
sink->last_id = sink->mem->id;
frame_set_data(frame, sink->mem->data, sink->mem->used);
FRAME_COPY_META(sink->mem, frame);
retval = 0;
}
sink->mem->last_client_ts = get_now_monotonic();
}
return retval;
done:
if (flock(sink->fd, LOCK_UN) < 0) {
LOG_PERROR("%s-sink: Can't unlock memory", sink->name);
return -1;
}
return retval;
}

View File

@@ -24,6 +24,7 @@
#include <stdint.h>
#include <stdbool.h>
#include <stdatomic.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
@@ -37,45 +38,31 @@
#include "tools.h"
#include "logging.h"
#include "frame.h"
#include "memsinksh.h"
#ifndef CFG_MEMSINK_MAX_DATA
# define CFG_MEMSINK_MAX_DATA 33554432
#endif
#define MEMSINK_MAX_DATA ((size_t)(CFG_MEMSINK_MAX_DATA))
typedef struct {
uint64_t id;
size_t used;
unsigned width;
unsigned height;
unsigned format;
unsigned stride;
bool online;
long double grab_ts;
long double encode_begin_ts;
long double encode_end_ts;
long double last_consumed_ts;
uint8_t data[MEMSINK_MAX_DATA];
} memsink_shared_s;
typedef struct {
const char *name;
const char *obj;
bool server;
bool rm;
unsigned client_ttl; // Only for server
unsigned timeout;
int fd;
memsink_shared_s *mem;
uint64_t last_id;
bool has_clients; // Only for server
atomic_bool has_clients; // Only for server
} memsink_s;
memsink_s *memsink_init(const char *name, const char *obj, bool server, mode_t mode, bool rm, unsigned timeout);
memsink_s *memsink_init(
const char *name, const char *obj, bool server,
mode_t mode, bool rm, unsigned client_ttl, unsigned timeout);
void memsink_destroy(memsink_s *sink);
bool memsink_server_check(memsink_s *sink, const frame_s *frame);
int memsink_server_put(memsink_s *sink, const frame_s *frame);
int memsink_client_get(memsink_s *sink, frame_s *frame);

84
src/libs/memsinksh.h Normal file
View File

@@ -0,0 +1,84 @@
/*****************************************************************************
# #
# 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 <stdint.h>
#include <stdbool.h>
#include <sys/types.h>
#include <sys/mman.h>
#define MEMSINK_MAGIC ((uint64_t)0xCAFEBABECAFEBABE)
#define MEMSINK_VERSION ((uint32_t)2)
#ifndef CFG_MEMSINK_MAX_DATA
# define CFG_MEMSINK_MAX_DATA 33554432
#endif
#define MEMSINK_MAX_DATA ((size_t)(CFG_MEMSINK_MAX_DATA))
typedef struct {
uint64_t magic;
uint32_t version;
uint64_t id;
size_t used;
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;
long double last_client_ts;
uint8_t data[MEMSINK_MAX_DATA];
} memsink_shared_s;
INLINE memsink_shared_s *memsink_shared_map(int fd) {
memsink_shared_s *mem = mmap(
NULL,
sizeof(memsink_shared_s),
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd,
0
);
if (mem == MAP_FAILED) {
return NULL;
}
assert(mem != NULL);
return mem;
}
INLINE int memsink_shared_unmap(memsink_shared_s *mem) {
assert(mem != NULL);
return munmap(mem, sizeof(memsink_shared_s));
}

39
src/libs/options.c Normal file
View File

@@ -0,0 +1,39 @@
/*****************************************************************************
# #
# 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 "options.h"
void build_short_options(const struct option opts[], char *short_opts, size_t size) {
memset(short_opts, 0, size);
for (unsigned short_index = 0, opt_index = 0; opts[opt_index].name != NULL; ++opt_index) {
assert(short_index < size - 3);
if (isalpha(opts[opt_index].val)) {
short_opts[short_index] = opts[opt_index].val;
++short_index;
if (opts[opt_index].has_arg == required_argument) {
short_opts[short_index] = ':';
++short_index;
}
}
}
}

33
src/libs/options.h Normal file
View File

@@ -0,0 +1,33 @@
/*****************************************************************************
# #
# 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 <string.h>
#include <ctype.h>
#include <getopt.h>
#include <assert.h>
#include <sys/types.h>
void build_short_options(const struct option opts[], char *short_opts, size_t size);

View File

@@ -26,24 +26,32 @@
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <unistd.h>
#include <locale.h>
#include <errno.h>
#include <math.h>
#include <time.h>
#include <assert.h>
#include <sys/file.h>
#define RN "\r\n"
#define INLINE inline __attribute__((always_inline))
#define UNUSED __attribute__((unused))
#define A_CALLOC(_dest, _nmemb) assert((_dest = calloc(_nmemb, sizeof(*(_dest)))))
#define A_REALLOC(_dest, _nmemb) assert((_dest = realloc(_dest, _nmemb * sizeof(*(_dest)))))
#define MEMSET_ZERO(_obj) memset(&(_obj), 0, sizeof(_obj))
#define A_ASPRINTF(_dest, _fmt, ...) assert(asprintf(&(_dest), _fmt, ##__VA_ARGS__) >= 0)
#define ARRAY_LEN(_array) (sizeof(_array) / sizeof(_array[0]))
#define INLINE inline __attribute__((always_inline))
#define UNUSED __attribute__((unused))
INLINE char *bool_to_string(bool flag) {
INLINE const char *bool_to_string(bool flag) {
return (flag ? "true" : "false");
}
@@ -102,10 +110,14 @@ INLINE long double get_now_monotonic(void) {
return (long double)sec + ((long double)msec) / 1000;
}
INLINE uint64_t get_now_id(void) {
INLINE uint64_t get_now_monotonic_u64(void) {
struct timespec ts;
assert(!clock_gettime(X_CLOCK_MONOTONIC, &ts));
uint64_t now = (uint64_t)(ts.tv_nsec / 1000) + (uint64_t)ts.tv_sec * 1000000;
return (uint64_t)(ts.tv_nsec / 1000) + (uint64_t)ts.tv_sec * 1000000;
}
INLINE uint64_t get_now_id(void) {
uint64_t now = get_now_monotonic_u64();
return (uint64_t)triple_u32(now) | ((uint64_t)triple_u32(now + 12345) << 32);
}
@@ -123,3 +135,32 @@ INLINE unsigned get_cores_available(void) {
cores_sysconf = (cores_sysconf < 0 ? 0 : cores_sysconf);
return max_u(min_u(cores_sysconf, 4), 1);
}
INLINE int flock_timedwait_monotonic(int fd, long double timeout) {
long double deadline_ts = get_now_monotonic() + timeout;
int retval = -1;
while (true) {
retval = flock(fd, LOCK_EX | LOCK_NB);
if (retval == 0 || errno != EWOULDBLOCK || get_now_monotonic() > deadline_ts) {
break;
}
if (usleep(1000) < 0) {
break;
}
}
return retval;
}
INLINE char *errno_to_string(int error, char *buf, size_t size) {
assert(buf);
assert(size > 0);
locale_t locale = newlocale(LC_MESSAGES_MASK, "C", NULL);
char *str = "!!! newlocale() error !!!";
strncpy(buf, (locale ? strerror_l(error, locale) : str), size - 1);
buf[size - 1] = '\0';
if (locale) {
freelocale(locale);
}
return buf;
}

View File

@@ -87,6 +87,6 @@ static void _jpeg_error_handler(j_common_ptr jpeg) {
char msg[JMSG_LENGTH_MAX];
(*jpeg_error->mgr.format_message)(jpeg, msg);
LOG_ERROR("Can't decompress %s JPEG: %s", jpeg_error->frame->name, msg);
LOG_ERROR("Can't decompress JPEG: %s", msg);
longjmp(jpeg_error->jmp, -1);
}

View File

@@ -44,7 +44,7 @@ frame_s *blank_frame_init(const char *path) {
}
static frame_s *_init_internal(void) {
frame_s *blank = frame_init("blank_internal");
frame_s *blank = frame_init();
frame_set_data(blank, BLANK_JPEG_DATA, BLANK_JPEG_DATA_SIZE);
blank->width = BLANK_JPEG_WIDTH;
blank->height = BLANK_JPEG_HEIGHT;
@@ -55,7 +55,7 @@ static frame_s *_init_internal(void) {
static frame_s *_init_external(const char *path) {
FILE *fp = NULL;
frame_s *blank = frame_init("blank_external");
frame_s *blank = frame_init();
blank->format = V4L2_PIX_FMT_JPEG;
if ((fp = fopen(path, "rb")) == NULL) {
@@ -83,7 +83,7 @@ static frame_s *_init_external(const char *path) {
}
# undef CHUNK_SIZE
frame_s *decoded = frame_init("blank_external_decoded");
frame_s *decoded = frame_init();
if (unjpeg(blank, decoded, false) < 0) {
frame_destroy(decoded);
goto error;

View File

@@ -12,30 +12,30 @@
<hr>
<ul>
<li>
<a href="/state"><b>/state</b></a><br>
<a href="state"><b>/state</b></a><br>
Get JSON structure with the state of the server.
</li>
<br>
<li>
<a href="/snapshot"><b>/snapshot</b></a><br>
<a href="snapshot"><b>/snapshot</b></a><br>
Get a current actual image from the server.
</li>
<br>
<li>
<a href="/stream"><b>/stream</b></a><br>
<a href="stream"><b>/stream</b></a><br>
Get a live stream. Query params:<br>
<br>
<ul>
<li>
<b>key=abc123</b><br>
The user-defined key, which is part of cookie <i>stream_client</i>, which allows<br>
the stream client to determine its identifier and view statistics using <a href="/state">/state</a>.
the stream client to determine its identifier and view statistics using <a href="state">/state</a>.
</li>
<br>
<li>
<b>extra_headers=1</b><br>
Add <i>X-UStreamer-*</i> headers to the <a href="/stream">/stream</a> handle
(like with the <a href="/snapshot">/snapshot</a>).
Add <i>X-UStreamer-*</i> headers to the <a href="stream">/stream</a> handle
(like with the <a href="snapshot">/snapshot</a>).
</li>
<br>
<li>
@@ -62,9 +62,9 @@
The mjpg-streamer compatibility layer:<br>
<br>
<ul>
<li><a href="/?action=snapshot">/?action=snapshot</a> as alias to the <a href="/snapshot">/snapshot</a>.</li>
<li><a href="?action=snapshot">/?action=snapshot</a> as alias to the <a href="snapshot">/snapshot</a>.</li>
<br>
<li><a href="/?action=stream">/?action=stream</a> as alias to the <a href="/stream">/stream</a>.</li>
<li><a href="?action=stream">/?action=stream</a> as alias to the <a href="stream">/stream</a>.</li>
</ul>
</li>
</ul>

View File

@@ -37,30 +37,30 @@ const char *const HTML_INDEX_PAGE = " \
<hr> \
<ul> \
<li> \
<a href=\"/state\"><b>/state</b></a><br> \
<a href=\"state\"><b>/state</b></a><br> \
Get JSON structure with the state of the server. \
</li> \
<br> \
<li> \
<a href=\"/snapshot\"><b>/snapshot</b></a><br> \
<a href=\"snapshot\"><b>/snapshot</b></a><br> \
Get a current actual image from the server. \
</li> \
<br> \
<li> \
<a href=\"/stream\"><b>/stream</b></a><br> \
<a href=\"stream\"><b>/stream</b></a><br> \
Get a live stream. Query params:<br> \
<br> \
<ul> \
<li> \
<b>key=abc123</b><br> \
The user-defined key, which is part of cookie <i>stream_client</i>, which allows<br> \
the stream client to determine its identifier and view statistics using <a href=\"/state\">/state</a>. \
the stream client to determine its identifier and view statistics using <a href=\"state\">/state</a>. \
</li> \
<br> \
<li> \
<b>extra_headers=1</b><br> \
Add <i>X-UStreamer-*</i> headers to the <a href=\"/stream\">/stream</a> handle \
(like with the <a href=\"/snapshot\">/snapshot</a>). \
Add <i>X-UStreamer-*</i> headers to the <a href=\"stream\">/stream</a> handle \
(like with the <a href=\"snapshot\">/snapshot</a>). \
</li> \
<br> \
<li> \
@@ -87,9 +87,9 @@ const char *const HTML_INDEX_PAGE = " \
The mjpg-streamer compatibility layer:<br> \
<br> \
<ul> \
<li><a href=\"/?action=snapshot\">/?action=snapshot</a> as alias to the <a href=\"/snapshot\">/snapshot</a>.</li> \
<li><a href=\"?action=snapshot\">/?action=snapshot</a> as alias to the <a href=\"snapshot\">/snapshot</a>.</li> \
<br> \
<li><a href=\"/?action=stream\">/?action=stream</a> as alias to the <a href=\"/stream\">/stream</a>.</li> \
<li><a href=\"?action=stream\">/?action=stream</a> as alias to the <a href=\"stream\">/stream</a>.</li> \
</ul> \
</li> \
</ul> \

View File

@@ -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},
};
@@ -852,6 +852,7 @@ static void _device_apply_controls(device_s *dev) {
CONTROL_AUTO_CID (V4L2_CID_AUTO_WHITE_BALANCE, V4L2_CID_WHITE_BALANCE_TEMPERATURE, white_balance);
CONTROL_AUTO_CID (V4L2_CID_AUTOGAIN, V4L2_CID_GAIN, gain);
CONTROL_MANUAL_CID ( V4L2_CID_COLORFX, color_effect);
CONTROL_MANUAL_CID ( V4L2_CID_ROTATE, rotate);
CONTROL_MANUAL_CID ( V4L2_CID_VFLIP, flip_vertical);
CONTROL_MANUAL_CID ( V4L2_CID_HFLIP, flip_horizontal);

View File

@@ -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"
@@ -122,6 +122,7 @@ typedef struct {
control_s white_balance;
control_s gain;
control_s color_effect;
control_s rotate;
control_s flip_vertical;
control_s flip_horizontal;
} controls_s;

View File

@@ -36,7 +36,7 @@ static const struct {
};
static void *_worker_job_init(worker_s *wr, void *v_enc);
static void *_worker_job_init(void *v_enc);
static void _worker_job_destroy(void *v_job);
static bool _worker_run_job(worker_s *wr);
@@ -212,23 +212,17 @@ void encoder_get_runtime_params(encoder_s *enc, encoder_type_e *type, unsigned *
A_MUTEX_UNLOCK(&ER(mutex));
}
static void *_worker_job_init(worker_s *wr, void *v_enc) {
static void *_worker_job_init(void *v_enc) {
encoder_job_s *job;
A_CALLOC(job, 1);
job->enc = (encoder_s *)v_enc;
const size_t dest_role_len = strlen(wr->name) + 16;
A_CALLOC(job->dest_role, dest_role_len);
snprintf(job->dest_role, dest_role_len, "%s_dest", wr->name);
job->dest = frame_init(job->dest_role);
job->dest = frame_init();
return (void *)job;
}
static void _worker_job_destroy(void *v_job) {
encoder_job_s *job = (encoder_job_s *)v_job;
frame_destroy(job->dest);
free(job->dest_role);
free(job);
}

View File

@@ -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;

View File

@@ -39,6 +39,7 @@
#include "../../../libs/logging.h"
#include "../../../libs/frame.h"
#include "vcos.h"
#include "formatters.h"
#include "component.h"

View 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;
}

View 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);

View File

@@ -23,7 +23,7 @@
#include "gpio.h"
gpio_s gpio = {
gpio_s us_gpio = {
.path = "/dev/gpiochip0",
.consumer_prefix = "ustreamer",
@@ -51,42 +51,42 @@ static void _gpio_output_destroy(gpio_output_s *output);
void gpio_init(void) {
assert(gpio.chip == NULL);
assert(us_gpio.chip == NULL);
if (
gpio.prog_running.pin >= 0
|| gpio.stream_online.pin >= 0
|| gpio.has_http_clients.pin >= 0
us_gpio.prog_running.pin >= 0
|| us_gpio.stream_online.pin >= 0
|| us_gpio.has_http_clients.pin >= 0
) {
A_MUTEX_INIT(&gpio.mutex);
LOG_INFO("GPIO: Using chip device: %s", gpio.path);
if ((gpio.chip = gpiod_chip_open(gpio.path)) != NULL) {
_gpio_output_init(&gpio.prog_running);
_gpio_output_init(&gpio.stream_online);
_gpio_output_init(&gpio.has_http_clients);
A_MUTEX_INIT(&us_gpio.mutex);
LOG_INFO("GPIO: Using chip device: %s", us_gpio.path);
if ((us_gpio.chip = gpiod_chip_open(us_gpio.path)) != NULL) {
_gpio_output_init(&us_gpio.prog_running);
_gpio_output_init(&us_gpio.stream_online);
_gpio_output_init(&us_gpio.has_http_clients);
} else {
LOG_PERROR("GPIO: Can't initialize chip device %s", gpio.path);
LOG_PERROR("GPIO: Can't initialize chip device %s", us_gpio.path);
}
}
}
void gpio_destroy(void) {
_gpio_output_destroy(&gpio.prog_running);
_gpio_output_destroy(&gpio.stream_online);
_gpio_output_destroy(&gpio.has_http_clients);
if (gpio.chip) {
gpiod_chip_close(gpio.chip);
gpio.chip = NULL;
A_MUTEX_DESTROY(&gpio.mutex);
_gpio_output_destroy(&us_gpio.prog_running);
_gpio_output_destroy(&us_gpio.stream_online);
_gpio_output_destroy(&us_gpio.has_http_clients);
if (us_gpio.chip) {
gpiod_chip_close(us_gpio.chip);
us_gpio.chip = NULL;
A_MUTEX_DESTROY(&us_gpio.mutex);
}
}
int gpio_inner_set(gpio_output_s *output, bool state) {
int retval = 0;
assert(gpio.chip);
assert(us_gpio.chip);
assert(output->line);
assert(output->state != state); // Must be checked in macro for the performance
A_MUTEX_LOCK(&gpio.mutex);
A_MUTEX_LOCK(&us_gpio.mutex);
if (gpiod_line_set_value(output->line, (int)state) < 0) { \
LOG_PERROR("GPIO: Can't write value %d to line %s (will be disabled)", state, output->consumer); \
@@ -94,19 +94,18 @@ int gpio_inner_set(gpio_output_s *output, bool state) {
retval = -1;
}
A_MUTEX_UNLOCK(&gpio.mutex);
A_MUTEX_UNLOCK(&us_gpio.mutex);
return retval;
}
static void _gpio_output_init(gpio_output_s *output) {
assert(gpio.chip);
assert(us_gpio.chip);
assert(output->line == NULL);
A_CALLOC(output->consumer, strlen(gpio.consumer_prefix) + strlen(output->role) + 16);
sprintf(output->consumer, "%s::%s", gpio.consumer_prefix, output->role);
A_ASPRINTF(output->consumer, "%s::%s", us_gpio.consumer_prefix, output->role);
if (output->pin >= 0) {
if ((output->line = gpiod_chip_get_line(gpio.chip, output->pin)) != NULL) {
if ((output->line = gpiod_chip_get_line(us_gpio.chip, output->pin)) != NULL) {
if (gpiod_line_request_output(output->line, output->consumer, 0) < 0) {
LOG_PERROR("GPIO: Can't request pin=%d as %s", output->pin, output->consumer);
_gpio_output_destroy(output);

View File

@@ -56,7 +56,7 @@ typedef struct {
} gpio_s;
extern gpio_s gpio;
extern gpio_s us_gpio;
void gpio_init(void);
@@ -73,15 +73,15 @@ int gpio_inner_set(gpio_output_s *output, bool state);
}
INLINE void gpio_set_prog_running(bool state) {
SET_STATE(gpio.prog_running, state);
SET_STATE(us_gpio.prog_running, state);
}
INLINE void gpio_set_stream_online(bool state) {
SET_STATE(gpio.stream_online, state);
SET_STATE(us_gpio.stream_online, state);
}
INLINE void gpio_set_has_http_clients(bool state) {
SET_STATE(gpio.has_http_clients, state);
SET_STATE(us_gpio.has_http_clients, state);
}
#undef SET_STATE

View File

@@ -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);

View File

@@ -38,6 +38,7 @@
#include "../../libs/tools.h"
#include "../../libs/logging.h"
#include "../../libs/frame.h"
#include "../encoders/omx/vcos.h"
typedef struct {

View File

@@ -27,12 +27,14 @@ h264_stream_s *h264_stream_init(memsink_s *sink, unsigned bitrate, unsigned gop)
h264_stream_s *h264;
A_CALLOC(h264, 1);
h264->sink = sink;
h264->tmp_src = frame_init("h264_tmp_src");
h264->dest = frame_init("h264_dest");
h264->tmp_src = frame_init();
h264->dest = frame_init();
atomic_init(&h264->online, false);
// FIXME: 30 or 0? https://github.com/6by9/yavta/blob/master/yavta.c#L210
if ((h264->enc = h264_encoder_init(bitrate, gop, 0)) == NULL) {
// FIXME: 30 or 0? https://github.com/6by9/yavta/blob/master/yavta.c#L2100
// По логике вещей правильно 0, но почему-то на низких разрешениях типа 640x480
// енкодер через несколько секунд перестает производить корректные фреймы.
if ((h264->enc = h264_encoder_init(bitrate, gop, 30)) == NULL) {
goto error;
}
@@ -53,6 +55,10 @@ void h264_stream_destroy(h264_stream_s *h264) {
}
void h264_stream_process(h264_stream_s *h264, const frame_s *frame, int vcsm_handle, bool force_key) {
if (!memsink_server_check(h264->sink, frame)) {
return;
}
long double now = get_now_monotonic();
bool zero_copy = false;

View File

@@ -25,6 +25,8 @@
static int _http_preprocess_request(struct evhttp_request *request, server_s *server);
static int _http_check_run_compat_action(struct evhttp_request *request, void *v_server);
static void _http_callback_root(struct evhttp_request *request, void *v_server);
static void _http_callback_static(struct evhttp_request *request, void *v_server);
static void _http_callback_state(struct evhttp_request *request, void *v_server);
@@ -39,6 +41,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)
@@ -49,7 +53,7 @@ static bool _expose_new_frame(server_s *server);
server_s *server_init(stream_s *stream) {
exposed_s *exposed;
A_CALLOC(exposed, 1);
exposed->frame = frame_init("http_exposed");
exposed->frame = frame_init();
server_runtime_s *run;
A_CALLOC(run, 1);
@@ -91,12 +95,11 @@ void server_destroy(server_s *server) {
libevent_global_shutdown();
# endif
for (stream_client_s *client = RUN(stream_clients); client != NULL;) {
stream_client_s *next = client->next;
LIST_ITERATE(RUN(stream_clients), client, {
free(client->key);
free(client->hostport);
free(client);
client = next;
}
});
if (RUN(auth_token)) {
free(RUN(auth_token));
@@ -142,16 +145,14 @@ int server_listen(server_s *server) {
evhttp_set_timeout(RUN(http), server->timeout);
if (server->user[0] != '\0') {
char *raw_token;
char *encoded_token = NULL;
A_CALLOC(raw_token, strlen(server->user) + strlen(server->passwd) + 16);
sprintf(raw_token, "%s:%s", server->user, server->passwd);
char *raw_token;
A_ASPRINTF(raw_token, "%s:%s", server->user, server->passwd);
base64_encode((uint8_t *)raw_token, strlen(raw_token), &encoded_token, NULL);
free(raw_token);
A_CALLOC(RUN(auth_token), strlen(encoded_token) + 16);
sprintf(RUN(auth_token), "Basic %s", encoded_token);
A_ASPRINTF(RUN(auth_token), "Basic %s", encoded_token);
free(encoded_token);
LOG_INFO("Using HTTP basic auth");
@@ -221,41 +222,63 @@ static int _http_preprocess_request(struct evhttp_request *request, server_s *se
} \
}
static void _http_callback_root(struct evhttp_request *request, void *v_server) {
server_s *server = (server_s *)v_server;
static int _http_check_run_compat_action(struct evhttp_request *request, void *v_server) {
// MJPG-Streamer compatibility layer
PREPROCESS_REQUEST;
struct evkeyvalq params;
int error = 0;
struct evkeyvalq params; // For mjpg-streamer compatibility
evhttp_parse_query(evhttp_request_get_uri(request), &params);
const char *action = evhttp_find_header(&params, "action");
if (action && !strcmp(action, "snapshot")) {
_http_callback_snapshot(request, v_server);
goto ok;
} else if (action && !strcmp(action, "stream")) {
_http_callback_stream(request, v_server);
} else {
struct evbuffer *buf;
assert((buf = evbuffer_new()));
assert(evbuffer_add_printf(buf, "%s", HTML_INDEX_PAGE));
ADD_HEADER("Content-Type", "text/html");
evhttp_send_reply(request, HTTP_OK, "OK", buf);
evbuffer_free(buf);
goto ok;
}
evhttp_clear_headers(&params);
error = -1;
ok:
evhttp_clear_headers(&params);
return error;
}
#define COMPAT_REQUEST { \
if (_http_check_run_compat_action(request, v_server) == 0) { \
return; \
} \
}
static void _http_callback_root(struct evhttp_request *request, void *v_server) {
server_s *server = (server_s *)v_server;
PREPROCESS_REQUEST;
COMPAT_REQUEST;
struct evbuffer *buf;
assert((buf = evbuffer_new()));
assert(evbuffer_add_printf(buf, "%s", HTML_INDEX_PAGE));
ADD_HEADER("Content-Type", "text/html");
evhttp_send_reply(request, HTTP_OK, "OK", buf);
evbuffer_free(buf);
}
static void _http_callback_static(struct evhttp_request *request, void *v_server) {
server_s *server = (server_s *)v_server;
PREPROCESS_REQUEST;
COMPAT_REQUEST;
struct evbuffer *buf = NULL;
struct evhttp_uri *uri = NULL;
char *decoded_path = NULL;
char *static_path = NULL;
int fd = -1;
PREPROCESS_REQUEST;
{
char *uri_path;
@@ -325,6 +348,8 @@ static void _http_callback_static(struct evhttp_request *request, void *v_server
}
}
#undef COMPAT_REQUEST
static void _http_callback_state(struct evhttp_request *request, void *v_server) {
server_s *server = (server_s *)v_server;
@@ -343,6 +368,7 @@ static void _http_callback_state(struct evhttp_request *request, void *v_server)
encoder_type_to_string(enc_type),
enc_quality
));
# ifdef WITH_OMX
if (STREAM(run->h264)) {
assert(evbuffer_add_printf(buf,
@@ -353,6 +379,32 @@ static void _http_callback_state(struct evhttp_request *request, void *v_server)
));
}
# endif
if (
STREAM(sink)
# ifdef WITH_OMX
|| STREAM(h264_sink)
# endif
) {
assert(evbuffer_add_printf(buf, " \"sinks\": {"));
if (STREAM(sink)) {
assert(evbuffer_add_printf(buf,
"\"jpeg\": {\"has_clients\": %s}",
bool_to_string(atomic_load(&STREAM(sink->has_clients)))
));
}
# ifdef WITH_OMX
if (STREAM(h264_sink)) {
assert(evbuffer_add_printf(buf,
"%s\"h264\": {\"has_clients\": %s}",
(STREAM(sink) ? ", " : ""),
bool_to_string(atomic_load(&STREAM(h264_sink->has_clients)))
));
}
# endif
assert(evbuffer_add_printf(buf, "},"));
}
assert(evbuffer_add_printf(buf,
" \"source\": {\"resolution\": {\"width\": %u, \"height\": %u},"
" \"online\": %s, \"desired_fps\": %u, \"captured_fps\": %u},"
@@ -366,9 +418,9 @@ static void _http_callback_state(struct evhttp_request *request, void *v_server)
RUN(stream_clients_count)
));
for (stream_client_s * client = RUN(stream_clients); client != NULL; client = client->next) {
LIST_ITERATE(RUN(stream_clients), client, {
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,
@@ -378,7 +430,7 @@ static void _http_callback_state(struct evhttp_request *request, void *v_server)
bool_to_string(client->zero_data),
(client->next ? ", " : "")
));
}
});
assert(evbuffer_add_printf(buf, "}}}}"));
@@ -473,20 +525,10 @@ static void _http_callback_stream(struct evhttp_request *request, void *v_server
# undef PARSE_PARAM
evhttp_clear_headers(&params);
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;
} else {
stream_client_s *last = RUN(stream_clients);
for (; last->next != NULL; last = last->next);
client->prev = last;
last->next = client;
}
RUN(stream_clients_count) += 1;
LIST_APPEND_C(RUN(stream_clients), client, RUN(stream_clients_count));
if (RUN(stream_clients_count) == 1) {
atomic_store(&VID(has_clients), true);
@@ -495,23 +537,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);
@@ -525,7 +562,6 @@ static void _http_callback_stream(struct evhttp_request *request, void *v_server
static void _http_callback_stream_write(struct bufferevent *buf_event, void *v_client) {
# define BOUNDARY "boundarydonotcross"
# define RN "\r\n"
stream_client_s *client = (stream_client_s *)v_client;
server_s *server = client->server;
@@ -575,7 +611,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,
@@ -650,7 +686,6 @@ static void _http_callback_stream_write(struct bufferevent *buf_event, void *v_c
bufferevent_enable(buf_event, EV_READ);
# undef ADD_ADVANCE_HEADERS
# undef RN
# undef BOUNDARY
}
@@ -658,10 +693,7 @@ static void _http_callback_stream_error(UNUSED struct bufferevent *buf_event, UN
stream_client_s *client = (stream_client_s *)v_client;
server_s *server = client->server;
char *reason = bufferevent_my_format_reason(what);
assert(RUN(stream_clients_count) > 0);
RUN(stream_clients_count) -= 1;
LIST_REMOVE_C(RUN(stream_clients), client, RUN(stream_clients_count));
if (RUN(stream_clients_count) == 0) {
atomic_store(&VID(has_clients), false);
@@ -670,40 +702,26 @@ static void _http_callback_stream_error(UNUSED struct bufferevent *buf_event, UN
# endif
}
char *client_addr = "???";
unsigned short client_port = 0;
char *reason = bufferevent_my_format_reason(what);
LOG_INFO("HTTP: Disconnected client: %s, id=%" PRIx64 ", %s; clients now: %u",
client->hostport, client->id, reason, RUN(stream_clients_count));
free(reason);
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);
}
if (client->prev == NULL) {
RUN(stream_clients) = client->next;
} else {
client->prev->next = client->next;
}
if (client->next != NULL) {
client->next->prev = client->prev;
}
free(client->key);
free(client->hostport);
free(client);
free(reason);
}
static void _http_queue_send_stream(server_s *server, bool stream_updated, bool frame_updated) {
bool has_clients = false;
bool queued = false;
for (stream_client_s *client = RUN(stream_clients); client != NULL; client = client->next) {
LIST_ITERATE(RUN(stream_clients), client, {
struct evhttp_connection *conn = evhttp_request_get_connection(client->request);
if (conn) {
// Фикс для бага WebKit. При включенной опции дропа одинаковых фреймов,
@@ -734,7 +752,7 @@ static void _http_queue_send_stream(server_s *server, bool stream_updated, bool
has_clients = true;
}
}
});
if (queued) {
static unsigned queued_fps_accum = 0;
@@ -836,3 +854,37 @@ 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;
A_ASPRINTF(hostport, "[%s]:%u", addr, port);
free(addr);
return hostport;
}

View File

@@ -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
@@ -59,6 +58,7 @@
#include "../../libs/process.h"
#include "../../libs/frame.h"
#include "../../libs/base64.h"
#include "../../libs/list.h"
#include "../data/index_html.h"
#include "../encoder.h"
#include "../stream.h"
@@ -83,7 +83,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;
@@ -91,8 +92,7 @@ typedef struct stream_client_sx {
unsigned fps_accum;
long long fps_accum_second;
struct stream_client_sx *prev;
struct stream_client_sx *next;
LIST_STRUCT(struct stream_client_sx);
} stream_client_s;
typedef struct {

View File

@@ -32,7 +32,7 @@ char *find_static_file_path(const char *root_path, const char *request_path) {
goto error;
}
A_CALLOC(path, strlen(root_path) + strlen(simplified_path) + 16);
A_CALLOC(path, strlen(root_path) + strlen(simplified_path) + 16); // + reserved for /index.html
sprintf(path, "%s/%s", root_path, simplified_path);
struct stat st;

View File

@@ -72,6 +72,7 @@ enum _OPT_VALUES {
_O_WHITE_BALANCE,
_O_GAIN,
_O_COLOR_EFFECT,
_O_ROTATE,
_O_FLIP_VERTICAL,
_O_FLIP_HORIZONTAL,
@@ -86,8 +87,10 @@ enum _OPT_VALUES {
_O_##_prefix, \
_O_##_prefix##_MODE, \
_O_##_prefix##_RM, \
_O_##_prefix##_CLIENT_TTL, \
_O_##_prefix##_TIMEOUT,
ADD_SINK(SINK)
ADD_SINK(RAW_SINK)
# ifdef WITH_OMX
ADD_SINK(H264_SINK)
_O_H264_BITRATE,
@@ -156,6 +159,7 @@ static const struct option _LONG_OPTS[] = {
{"white-balance", required_argument, NULL, _O_WHITE_BALANCE},
{"gain", required_argument, NULL, _O_GAIN},
{"color-effect", required_argument, NULL, _O_COLOR_EFFECT},
{"rotate", required_argument, NULL, _O_ROTATE},
{"flip-vertical", required_argument, NULL, _O_FLIP_VERTICAL},
{"flip-horizontal", required_argument, NULL, _O_FLIP_HORIZONTAL},
@@ -174,11 +178,13 @@ static const struct option _LONG_OPTS[] = {
{"server-timeout", required_argument, NULL, _O_SERVER_TIMEOUT},
# define ADD_SINK(_opt, _prefix) \
{_opt "sink", required_argument, NULL, _O_##_prefix}, \
{_opt "sink-mode", required_argument, NULL, _O_##_prefix##_MODE}, \
{_opt "sink-rm", no_argument, NULL, _O_##_prefix##_RM}, \
{_opt "sink-timeout", required_argument, NULL, _O_##_prefix##_TIMEOUT},
{_opt "sink", required_argument, NULL, _O_##_prefix}, \
{_opt "sink-mode", required_argument, NULL, _O_##_prefix##_MODE}, \
{_opt "sink-rm", no_argument, NULL, _O_##_prefix##_RM}, \
{_opt "sink-client-ttl", required_argument, NULL, _O_##_prefix##_CLIENT_TTL}, \
{_opt "sink-timeout", required_argument, NULL, _O_##_prefix##_TIMEOUT},
ADD_SINK("", SINK)
ADD_SINK("raw-", RAW_SINK)
# ifdef WITH_OMX
ADD_SINK("h264-", H264_SINK)
{"h264-bitrate", required_argument, NULL, _O_H264_BITRATE},
@@ -243,6 +249,7 @@ void options_destroy(options_s *options) {
} \
}
ADD_SINK(sink);
ADD_SINK(raw_sink);
# ifdef WITH_OMX
ADD_SINK(h264_sink);
# endif
@@ -334,8 +341,10 @@ int options_parse(options_s *options, device_s *dev, encoder_s *enc, stream_s *s
char *_prefix##_name = NULL; \
mode_t _prefix##_mode = 0660; \
bool _prefix##_rm = false; \
unsigned _prefix##_client_ttl = 10; \
unsigned _prefix##_timeout = 1;
ADD_SINK(sink);
ADD_SINK(raw_sink);
# ifdef WITH_OMX
ADD_SINK(h264_sink);
# endif
@@ -345,18 +354,8 @@ int options_parse(options_s *options, device_s *dev, encoder_s *enc, stream_s *s
char *process_name_prefix = NULL;
# endif
char short_opts[1024] = {0};
for (int short_index = 0, opt_index = 0; _LONG_OPTS[opt_index].name != NULL; ++opt_index) {
if (isalpha(_LONG_OPTS[opt_index].val)) {
short_opts[short_index] = _LONG_OPTS[opt_index].val;
++short_index;
if (_LONG_OPTS[opt_index].has_arg == required_argument) {
short_opts[short_index] = ':';
++short_index;
}
}
}
char short_opts[128];
build_short_options(_LONG_OPTS, short_opts, 128);
for (int ch; (ch = getopt_long(options->argc, options->argv_copy, short_opts, _LONG_OPTS, NULL)) >= 0;) {
switch (ch) {
@@ -397,6 +396,7 @@ int options_parse(options_s *options, device_s *dev, encoder_s *enc, stream_s *s
OPT_CTL_DEFAULT_NOBREAK(white_balance);
OPT_CTL_DEFAULT_NOBREAK(gain);
OPT_CTL_DEFAULT_NOBREAK(color_effect);
OPT_CTL_DEFAULT_NOBREAK(rotate);
OPT_CTL_DEFAULT_NOBREAK(flip_vertical);
OPT_CTL_DEFAULT_NOBREAK(flip_horizontal);
break;
@@ -410,6 +410,7 @@ int options_parse(options_s *options, device_s *dev, encoder_s *enc, stream_s *s
case _O_WHITE_BALANCE: OPT_CTL_AUTO(white_balance);
case _O_GAIN: OPT_CTL_AUTO(gain);
case _O_COLOR_EFFECT: OPT_CTL_MANUAL(color_effect);
case _O_ROTATE: OPT_CTL_MANUAL(rotate);
case _O_FLIP_VERTICAL: OPT_CTL_MANUAL(flip_vertical);
case _O_FLIP_HORIZONTAL: OPT_CTL_MANUAL(flip_horizontal);
@@ -431,21 +432,23 @@ int options_parse(options_s *options, device_s *dev, encoder_s *enc, stream_s *s
case _O_##_up: OPT_SET(_lp##_name, optarg); \
case _O_##_up##_MODE: OPT_NUMBER("--" #_opt "sink-mode", _lp##_mode, INT_MIN, INT_MAX, 8); \
case _O_##_up##_RM: OPT_SET(_lp##_rm, true); \
case _O_##_up##_CLIENT_TTL: OPT_NUMBER("--" #_opt "sink-client-ttl", _lp##_client_ttl, 1, 60, 0); \
case _O_##_up##_TIMEOUT: OPT_NUMBER("--" #_opt "sink-timeout", _lp##_timeout, 1, 60, 0);
ADD_SINK("", sink, SINK)
ADD_SINK("raw-", raw_sink, RAW_SINK)
# ifdef WITH_OMX
ADD_SINK("h264-", h264_sink, H264_SINK)
case _O_H264_BITRATE: OPT_NUMBER("--h264-bitrate", stream->h264_bitrate, 100, 16000, 0);
case _O_H264_BITRATE: OPT_NUMBER("--h264-bitrate", stream->h264_bitrate, 25, 25000, 0);
case _O_H264_GOP: OPT_NUMBER("--h264-gop", stream->h264_gop, 0, 60, 0);
# endif
# undef ADD_SINK
# ifdef WITH_GPIO
case _O_GPIO_DEVICE: OPT_SET(gpio.path, optarg);
case _O_GPIO_CONSUMER_PREFIX: OPT_SET(gpio.consumer_prefix, optarg);
case _O_GPIO_PROG_RUNNING: OPT_NUMBER("--gpio-prog-running", gpio.prog_running.pin, 0, 256, 0);
case _O_GPIO_STREAM_ONLINE: OPT_NUMBER("--gpio-stream-online", gpio.stream_online.pin, 0, 256, 0);
case _O_GPIO_HAS_HTTP_CLIENTS: OPT_NUMBER("--gpio-has-http-clients", gpio.has_http_clients.pin, 0, 256, 0);
case _O_GPIO_DEVICE: OPT_SET(us_gpio.path, optarg);
case _O_GPIO_CONSUMER_PREFIX: OPT_SET(us_gpio.consumer_prefix, optarg);
case _O_GPIO_PROG_RUNNING: OPT_NUMBER("--gpio-prog-running", us_gpio.prog_running.pin, 0, 256, 0);
case _O_GPIO_STREAM_ONLINE: OPT_NUMBER("--gpio-stream-online", us_gpio.stream_online.pin, 0, 256, 0);
case _O_GPIO_HAS_HTTP_CLIENTS: OPT_NUMBER("--gpio-has-http-clients", us_gpio.has_http_clients.pin, 0, 256, 0);
# endif
# ifdef HAS_PDEATHSIG
@@ -460,19 +463,19 @@ int options_parse(options_s *options, device_s *dev, encoder_s *enc, stream_s *s
# endif
case _O_NOTIFY_PARENT: OPT_SET(server->notify_parent, true);
case _O_LOG_LEVEL: OPT_NUMBER("--log-level", log_level, LOG_LEVEL_INFO, LOG_LEVEL_DEBUG, 0);
case _O_PERF: OPT_SET(log_level, LOG_LEVEL_PERF);
case _O_VERBOSE: OPT_SET(log_level, LOG_LEVEL_VERBOSE);
case _O_DEBUG: OPT_SET(log_level, LOG_LEVEL_DEBUG);
case _O_FORCE_LOG_COLORS: OPT_SET(log_colored, true);
case _O_NO_LOG_COLORS: OPT_SET(log_colored, false);
case _O_LOG_LEVEL: OPT_NUMBER("--log-level", us_log_level, LOG_LEVEL_INFO, LOG_LEVEL_DEBUG, 0);
case _O_PERF: OPT_SET(us_log_level, LOG_LEVEL_PERF);
case _O_VERBOSE: OPT_SET(us_log_level, LOG_LEVEL_VERBOSE);
case _O_DEBUG: OPT_SET(us_log_level, LOG_LEVEL_DEBUG);
case _O_FORCE_LOG_COLORS: OPT_SET(us_log_colored, true);
case _O_NO_LOG_COLORS: OPT_SET(us_log_colored, false);
case _O_HELP: _help(stdout, dev, enc, stream, server); return 1;
case _O_VERSION: puts(VERSION); return 1;
case _O_FEATURES: _features(); return 1;
case 0: break;
default: _help(stderr, dev, enc, stream, server); return -1;
default: return -1;
}
}
@@ -487,12 +490,14 @@ int options_parse(options_s *options, device_s *dev, encoder_s *enc, stream_s *s
true, \
_prefix##_mode, \
_prefix##_rm, \
_prefix##_client_ttl, \
_prefix##_timeout \
); \
} \
stream->_prefix = options->_prefix; \
}
ADD_SINK("JPEG", sink);
ADD_SINK("RAW", raw_sink);
# ifdef WITH_OMX
ADD_SINK("H264", h264_sink);
# endif
@@ -616,7 +621,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);
@@ -635,6 +641,7 @@ static void _help(FILE *fp, device_s *dev, encoder_s *enc, stream_s *stream, ser
SAY(" --white-balance <N|auto|default> ───── Set white balance. Default: no change.\n");
SAY(" --gain <N|auto|default> ────────────── Set gain. Default: no change.\n");
SAY(" --color-effect <N|default> ─────────── Set color effect. Default: no change.\n");
SAY(" --rotate <N|default> ───────────────── Set rotation. Default: no change.\n");
SAY(" --flip-vertical <1|0|default> ──────── Set vertical flip. Default: no change.\n");
SAY(" --flip-horizontal <1|0|default> ────── Set horizontal flip. Default: no change.\n");
SAY(" Hint: use v4l2-ctl --list-ctrls-menus to query available controls of the device.\n");
@@ -661,22 +668,24 @@ static void _help(FILE *fp, device_s *dev, encoder_s *enc, stream_s *stream, ser
# define ADD_SINK(_name, _opt) \
SAY(_name " sink options:"); \
SAY("══════════════════"); \
SAY(" --" _opt "sink <name> ──────── Use the shared memory to sink " _name " frames. Default: disabled.\n"); \
SAY(" --" _opt "sink-mode <mode> ─── Set " _name " sink permissions (like 777). Default: 660.\n"); \
SAY(" --" _opt "sink-rm ──────────── Remove shared memory on stop. Default: disabled.\n"); \
SAY(" --" _opt "sink-timeout <sec> ─ Timeout for lock. Default: 1.\n");
SAY(" --" _opt "sink <name> ─────────── Use the shared memory to sink " _name " frames. Default: disabled.\n"); \
SAY(" --" _opt "sink-mode <mode> ────── Set " _name " sink permissions (like 777). Default: 660.\n"); \
SAY(" --" _opt "sink-rm ─────────────── Remove shared memory on stop. Default: disabled.\n"); \
SAY(" --" _opt "sink-client-ttl <sec> ─ Client TTL. Default: 10.\n"); \
SAY(" --" _opt "sink-timeout <sec> ──── Timeout for lock. Default: 1.\n");
ADD_SINK("JPEG", "")
ADD_SINK("RAW", "raw-")
# ifdef WITH_OMX
ADD_SINK("H264", "h264-")
SAY(" --h264-bitrate <kbps> ───── H264 bitrate in Kbps. Default: %u.\n", stream->h264_bitrate);
SAY(" --h264-gop <N> ──────────── Intarval between keyframes. Default: %u.\n", stream->h264_gop);
SAY(" --h264-bitrate <kbps> ──────── H264 bitrate in Kbps. Default: %u.\n", stream->h264_bitrate);
SAY(" --h264-gop <N> ─────────────── Intarval between keyframes. Default: %u.\n", stream->h264_gop);
# endif
# undef ADD_SINK
# ifdef WITH_GPIO
SAY("GPIO options:");
SAY("═════════════");
SAY(" --gpio-device </dev/path> ───── Path to GPIO character device. Default: %s.\n", gpio.path);
SAY(" --gpio-consumer-prefix <str> ── Consumer prefix for GPIO outputs. Default: %s.\n", gpio.consumer_prefix);
SAY(" --gpio-device </dev/path> ───── Path to GPIO character device. Default: %s.\n", us_gpio.path);
SAY(" --gpio-consumer-prefix <str> ── Consumer prefix for GPIO outputs. Default: %s.\n", us_gpio.consumer_prefix);
SAY(" --gpio-prog-running <pin> ───── Set 1 on GPIO pin while uStreamer is running. Default: disabled.\n");
SAY(" --gpio-stream-online <pin> ──── Set 1 while streaming. Default: disabled.\n");
SAY(" --gpio-has-http-clients <pin> ─ Set 1 while stream has at least one client. Default: disabled.\n");
@@ -699,7 +708,7 @@ static void _help(FILE *fp, device_s *dev, encoder_s *enc, stream_s *stream, ser
SAY(" --log-level <N> ──── Verbosity level of messages from 0 (info) to 3 (debug).");
SAY(" Enabling debugging messages can slow down the program.");
SAY(" Available levels: 0 (info), 1 (performance), 2 (verbose), 3 (debug).");
SAY(" Default: %d.\n", log_level);
SAY(" Default: %d.\n", us_log_level);
SAY(" --perf ───────────── Enable performance messages (same as --log-level=1). Default: disabled.\n");
SAY(" --verbose ────────── Enable verbose messages and lower (same as --log-level=2). Default: disabled.\n");
SAY(" --debug ──────────── Enable debug messages and lower (same as --log-level=3). Default: disabled.\n");

View File

@@ -27,7 +27,6 @@
#include <stdbool.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <limits.h>
#include <getopt.h>
#include <errno.h>
@@ -38,6 +37,7 @@
#include "../libs/process.h"
#include "../libs/frame.h"
#include "../libs/memsink.h"
#include "../libs/options.h"
#include "device.h"
#include "encoder.h"
@@ -55,6 +55,7 @@ typedef struct {
char **argv_copy;
frame_s *blank;
memsink_s *sink;
memsink_s *raw_sink;
# ifdef WITH_OMX
memsink_s *h264_sink;
# endif

View File

@@ -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;
@@ -38,7 +52,7 @@ stream_s *stream_init(device_s *dev, encoder_s *enc) {
video_s *video;
A_CALLOC(video, 1);
video->frame = frame_init("stream_video");
video->frame = frame_init();
atomic_init(&video->updated, false);
A_MUTEX_INIT(&video->mutex);
atomic_init(&video->has_clients, false);
@@ -122,9 +136,9 @@ void stream_loop(stream_s *stream) {
&& !atomic_load(&RUN(stop))
&& !atomic_load(&RUN(video->has_clients))
// has_clients синков НЕ обновляются в реальном времени
&& (stream->sink == NULL || !stream->sink->has_clients)
&& (stream->sink == NULL || !atomic_load(&stream->sink->has_clients))
# ifdef WITH_OMX
&& (RUN(h264) == NULL || /*RUN(h264->sink) == NULL ||*/ !RUN(h264->sink->has_clients))
&& (RUN(h264) == NULL || /*RUN(h264->sink) == NULL ||*/ !atomic_load(&RUN(h264->sink->has_clients)))
# endif
) {
usleep(100000);
@@ -194,10 +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);
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
@@ -247,13 +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)) {
# 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) {
@@ -297,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));
@@ -311,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");
@@ -321,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");
@@ -339,24 +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) {
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

View File

@@ -80,6 +80,7 @@ typedef struct {
unsigned error_delay;
memsink_s *sink;
memsink_s *raw_sink;
# ifdef WITH_OMX
memsink_s *h264_sink;

View File

@@ -28,9 +28,9 @@ static void *_worker_thread(void *v_worker);
workers_pool_s *workers_pool_init(
const char *name, const char *wr_prefix, unsigned n_workers, long double desired_interval,
void *(*job_init)(worker_s *wr, void *arg), void *job_init_arg,
void (*job_destroy)(void *),
bool (*run_job)(worker_s *)) {
workers_pool_job_init_f job_init, void *job_init_arg,
workers_pool_job_destroy_f job_destroy,
workers_pool_run_job_f run_job) {
LOG_INFO("Creating pool %s with %u workers ...", name, n_workers);
@@ -49,21 +49,18 @@ workers_pool_s *workers_pool_init(
A_MUTEX_INIT(&pool->free_workers_mutex);
A_COND_INIT(&pool->free_workers_cond);
const size_t wr_name_len = strlen(wr_prefix) + 64;
for (unsigned number = 0; number < pool->n_workers; ++number) {
# define WR(_next) pool->workers[number]._next
WR(number) = number;
A_CALLOC(WR(name), wr_name_len);
snprintf(WR(name), wr_name_len, "%s-%u", wr_prefix, number);
A_ASPRINTF(WR(name), "%s-%u", wr_prefix, number);
A_MUTEX_INIT(&WR(has_job_mutex));
atomic_init(&WR(has_job), false);
A_COND_INIT(&WR(has_job_cond));
WR(pool) = pool;
WR(job) = job_init(&pool->workers[number], job_init_arg);
WR(job) = job_init(job_init_arg);
A_THREAD_CREATE(&WR(tid), _worker_thread, (void *)&(pool->workers[number]));
pool->free_workers += 1;

View File

@@ -55,12 +55,16 @@ typedef struct worker_sx {
struct workers_pool_sx *pool;
} worker_s;
typedef void *(*workers_pool_job_init_f)(void *arg);
typedef void (*workers_pool_job_destroy_f)(void *job);
typedef bool (*workers_pool_run_job_f)(worker_s *wr);
typedef struct workers_pool_sx {
const char *name;
long double desired_interval;
bool (*run_job)(worker_s *wr);
void (*job_destroy)(void *job);
workers_pool_job_destroy_f job_destroy;
workers_pool_run_job_f run_job;
unsigned n_workers;
worker_s *workers;
@@ -79,9 +83,9 @@ typedef struct workers_pool_sx {
workers_pool_s *workers_pool_init(
const char *name, const char *wr_prefix, unsigned n_workers, long double desired_interval,
void *(*job_init)(worker_s *wr, void *arg), void *job_init_arg,
void (*job_destroy)(void *job),
bool (*run_job)(worker_s *));
workers_pool_job_init_f job_init, void *job_init_arg,
workers_pool_job_destroy_f job_destroy,
workers_pool_run_job_f run_job);
void workers_pool_destroy(workers_pool_s *pool);