mirror of
https://github.com/pikvm/ustreamer.git
synced 2026-11-04 20:14:24 +00:00
Compare commits
425 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84dc156b4a | ||
|
|
f627948a04 | ||
|
|
72bfd68514 | ||
|
|
6273fc187a | ||
|
|
5277bc7c59 | ||
|
|
6e0f358fbe | ||
|
|
3106aa7ef0 | ||
|
|
64981efb73 | ||
|
|
2823d811ef | ||
|
|
35fa72ba73 | ||
|
|
e1542dd617 | ||
|
|
6e554e5081 | ||
|
|
b5e12a8412 | ||
|
|
c65004f368 | ||
|
|
fc8550da6f | ||
|
|
604d6613c5 | ||
|
|
5a189866b7 | ||
|
|
db96c0699d | ||
|
|
2e07557c8c | ||
|
|
328e550801 | ||
|
|
b09efaacb8 | ||
|
|
dae9301e63 | ||
|
|
0301608b8f | ||
|
|
dd89d04211 | ||
|
|
6dd5b8709f | ||
|
|
41a8eb46ca | ||
|
|
7cf03cf35a | ||
|
|
654efeeca2 | ||
|
|
0f96532167 | ||
|
|
95566d51df | ||
|
|
b1c5276ab4 | ||
|
|
ce231e7963 | ||
|
|
ff639cce1d | ||
|
|
fdea0d214b | ||
|
|
72e2c6dabe | ||
|
|
2c515eeea5 | ||
|
|
730cc3c89e | ||
|
|
686460fa29 | ||
|
|
54c1c77dfb | ||
|
|
0e0aef7b80 | ||
|
|
31b98f1d59 | ||
|
|
9734c83bd4 | ||
|
|
b88abdcae4 | ||
|
|
6fca17cf52 | ||
|
|
5fe579956e | ||
|
|
52dfa38a8f | ||
|
|
8b76b59393 | ||
|
|
92878e22ab | ||
|
|
4dc0f73e1b | ||
|
|
9fe42b038a | ||
|
|
3750a22091 | ||
|
|
ef59c52a83 | ||
|
|
f296bb57e7 | ||
|
|
f7166b7c41 | ||
|
|
b35200345e | ||
|
|
5bd7c9c5ef | ||
|
|
fc35b1cd87 | ||
|
|
92d9828894 | ||
|
|
e7008c6086 | ||
|
|
d0672e15b9 | ||
|
|
04aa4c166c | ||
|
|
0fbbc27d8c | ||
|
|
b02bb85d8b | ||
|
|
80dce276ed | ||
|
|
e869536a11 | ||
|
|
b0acf0fa4d | ||
|
|
f32e2569d2 | ||
|
|
6d63f8e5d2 | ||
|
|
2dac4db9fc | ||
|
|
23dd2f9e66 | ||
|
|
fdfd4225d0 | ||
|
|
9424b655eb | ||
|
|
f94fe25845 | ||
|
|
bdcbaae8b3 | ||
|
|
52a2326426 | ||
|
|
fa15d9693f | ||
|
|
88460b72e1 | ||
|
|
8c69c77481 | ||
|
|
5331ae14aa | ||
|
|
0127dcf018 | ||
|
|
aa58b1b002 | ||
|
|
a05eab71a8 | ||
|
|
e013356cf0 | ||
|
|
c730981827 | ||
|
|
3bb1ed3ef3 | ||
|
|
1cda22bfd2 | ||
|
|
29ee20e864 | ||
|
|
5e13f4cd58 | ||
|
|
39c1916f61 | ||
|
|
614e83771b | ||
|
|
15a9e28ac6 | ||
|
|
a1ae02de5d | ||
|
|
214708549d | ||
|
|
1be4521664 | ||
|
|
94752dde75 | ||
|
|
b933b7b407 | ||
|
|
61f44b5f97 | ||
|
|
8fef0408b6 | ||
|
|
62028be064 | ||
|
|
3b7592bb31 | ||
|
|
8adca998e9 | ||
|
|
6ac5a5f065 | ||
|
|
12cf4492bd | ||
|
|
a6f111f7cf | ||
|
|
efbb2aa7ba | ||
|
|
5692d81e46 | ||
|
|
4cec824b13 | ||
|
|
ac1989451c | ||
|
|
e39d27309a | ||
|
|
b983b6c355 | ||
|
|
5204f00812 | ||
|
|
9eb39bbfc3 | ||
|
|
6adbb93e57 | ||
|
|
4bd1465a10 | ||
|
|
cf7f8947ef | ||
|
|
ec2e6c313b | ||
|
|
de2cfa36e1 | ||
|
|
6c1a8f75a1 | ||
|
|
26ee5143ee | ||
|
|
e2890e5851 | ||
|
|
e2b01e4d79 | ||
|
|
903bc45bee | ||
|
|
b2b1989c5b | ||
|
|
36b539c275 | ||
|
|
38c6917644 | ||
|
|
05a5d3fed4 | ||
|
|
0e4bf31325 | ||
|
|
9a5cce3b92 | ||
|
|
c4ac67acba | ||
|
|
472673ea90 | ||
|
|
f7ebe31c71 | ||
|
|
3a831817f4 | ||
|
|
913cdac7a6 | ||
|
|
777697dc1e | ||
|
|
5f437b9a35 | ||
|
|
b089f896da | ||
|
|
0e521ad0c6 | ||
|
|
620a0ec847 | ||
|
|
7a1d4816ed | ||
|
|
aec8431024 | ||
|
|
5b18e29555 | ||
|
|
2717248581 | ||
|
|
afd305e87d | ||
|
|
e3d8132237 | ||
|
|
1f32e875c3 | ||
|
|
2e88fb9294 | ||
|
|
d68f8e6d86 | ||
|
|
b380beba6d | ||
|
|
3a06a484ce | ||
|
|
0307d3bdb6 | ||
|
|
f2dd9c3c5a | ||
|
|
4e3f873f0d | ||
|
|
029440cf82 | ||
|
|
df74f5cf18 | ||
|
|
97494c3531 | ||
|
|
71544880d1 | ||
|
|
83127e58ff | ||
|
|
604a8f7cb4 | ||
|
|
602c1747d5 | ||
|
|
a2b8b35070 | ||
|
|
dd7701be38 | ||
|
|
1c9bd91b31 | ||
|
|
e19a3ca7ff | ||
|
|
b2d1a5612d | ||
|
|
f3e0613de3 | ||
|
|
5baf921660 | ||
|
|
6cabcd39f1 | ||
|
|
3df3658e4f | ||
|
|
f21fc5f6d3 | ||
|
|
b70ed98af9 | ||
|
|
52cdabe150 | ||
|
|
fe86997d08 | ||
|
|
df39b824c6 | ||
|
|
db297db52e | ||
|
|
b304364af9 | ||
|
|
ddec4e8478 | ||
|
|
28ca658621 | ||
|
|
270d3ae3a9 | ||
|
|
c1f080f29f | ||
|
|
b1e7c82131 | ||
|
|
3d7685ac48 | ||
|
|
37e79995fe | ||
|
|
1ee096b17c | ||
|
|
918688e91d | ||
|
|
a94ff667b0 | ||
|
|
10595a13e9 | ||
|
|
80ffc8b2bd | ||
|
|
ba246d90c0 | ||
|
|
29c98e3908 | ||
|
|
acc8cecbe4 | ||
|
|
8c31af2f03 | ||
|
|
a727c9b7c5 | ||
|
|
eabc8d8343 | ||
|
|
4e4ae21a83 | ||
|
|
412a1775a6 | ||
|
|
c404c49c6d | ||
|
|
481e359153 | ||
|
|
04114bba86 | ||
|
|
c848756d53 | ||
|
|
2a8aaabe48 | ||
|
|
239db92a85 | ||
|
|
740e09c70d | ||
|
|
e030479aae | ||
|
|
4db730abd9 | ||
|
|
79020143c7 | ||
|
|
1f96925181 | ||
|
|
74dc1dc146 | ||
|
|
6f8e8205b3 | ||
|
|
5f932d862b | ||
|
|
590a73f9ec | ||
|
|
79bbafdc98 | ||
|
|
fcecc12229 | ||
|
|
f79a663839 | ||
|
|
3e228c1fb8 | ||
|
|
53ec87b416 | ||
|
|
de8cb85605 | ||
|
|
000be92a0b | ||
|
|
f2779f7b44 | ||
|
|
dcddfddf56 | ||
|
|
793f24c48e | ||
|
|
25d87d5fa8 | ||
|
|
e8a7fb32ac | ||
|
|
9d5eb8bacb | ||
|
|
353e58d7ca | ||
|
|
6c24c9ea61 | ||
|
|
dfeefe5a1c | ||
|
|
aae090ab4e | ||
|
|
18038799f0 | ||
|
|
fab4c47f17 | ||
|
|
c40b3ee225 | ||
|
|
fca69db680 | ||
|
|
0d974a5faf | ||
|
|
1ed39790ba | ||
|
|
75a193f997 | ||
|
|
65c652e624 | ||
|
|
ae2f270f50 | ||
|
|
0a639eabca | ||
|
|
9ec59143dd | ||
|
|
e059a21ef9 | ||
|
|
074ce86f67 | ||
|
|
b8b67de5cf | ||
|
|
5f3198e72f | ||
|
|
3a3889d02c | ||
|
|
88203f9c53 | ||
|
|
24aca349a3 | ||
|
|
a9e0cb49e9 | ||
|
|
4ec3f11935 | ||
|
|
14e9d9f7af | ||
|
|
580ca68291 | ||
|
|
37f3f093dc | ||
|
|
70fa6548fe | ||
|
|
f8a703f166 | ||
|
|
3f69dd785f | ||
|
|
8e6c374acf | ||
|
|
caf9ed7bfe | ||
|
|
94b1224456 | ||
|
|
c8201df720 | ||
|
|
e0f09f65a1 | ||
|
|
4e1f62bfac | ||
|
|
b0b881f199 | ||
|
|
a21f527bce | ||
|
|
d64077c2d5 | ||
|
|
83f12baa61 | ||
|
|
b6fac2608d | ||
|
|
e6ebc12505 | ||
|
|
8c92ab6f47 | ||
|
|
7dc492d875 | ||
|
|
d43014346d | ||
|
|
bcd447963c | ||
|
|
eec6cfd0d4 | ||
|
|
f177300e69 | ||
|
|
7015a26a63 | ||
|
|
290282b6b6 | ||
|
|
a339ff5d06 | ||
|
|
8d4e9a6ca0 | ||
|
|
f0f5fcd67f | ||
|
|
02fabc7b45 | ||
|
|
bdf55396e5 | ||
|
|
976f466038 | ||
|
|
b79b4fd55e | ||
|
|
6d77f5334f | ||
|
|
25957de017 | ||
|
|
847f34e10c | ||
|
|
0ab8e0d05e | ||
|
|
ac88996a8c | ||
|
|
408157c82b | ||
|
|
7356dea737 | ||
|
|
87a75a816a | ||
|
|
b6a2332207 | ||
|
|
34c0dcb1ce | ||
|
|
283f31a5a6 | ||
|
|
2f1264c916 | ||
|
|
69e7cbf746 | ||
|
|
05804e309f | ||
|
|
1d8c93d3ad | ||
|
|
f48695a04e | ||
|
|
8ac2fa201b | ||
|
|
2d9e51a1ca | ||
|
|
c4cf4f015b | ||
|
|
646afbffff | ||
|
|
6475eeef4c | ||
|
|
2e67a46eb8 | ||
|
|
c333e75dff | ||
|
|
72285023cb | ||
|
|
b00a6ffd8d | ||
|
|
ce935c431e | ||
|
|
ac0944ae1a | ||
|
|
66572806a2 | ||
|
|
a75d6487e3 | ||
|
|
897ad4951b | ||
|
|
e1ef86146f | ||
|
|
8f3a475a32 | ||
|
|
be5f63d64d | ||
|
|
40e17b05b3 | ||
|
|
0b8940d93d | ||
|
|
e92002c3d8 | ||
|
|
e558b0f1a1 | ||
|
|
b5784149b2 | ||
|
|
55b6a3e933 | ||
|
|
f7c2948477 | ||
|
|
c55b6c4d7d | ||
|
|
442790486c | ||
|
|
bbc7ceb110 | ||
|
|
2ffa561eb1 | ||
|
|
490d833983 | ||
|
|
0b3a1eb963 | ||
|
|
7fd5eb229f | ||
|
|
98b5e52a68 | ||
|
|
c8dc5119fe | ||
|
|
b556dfb897 | ||
|
|
06eda04180 | ||
|
|
05bba86c63 | ||
|
|
6827a72097 | ||
|
|
299b3886af | ||
|
|
f9bc5666b8 | ||
|
|
c9cb0a416e | ||
|
|
ffa68a86a6 | ||
|
|
8fe411aa8b | ||
|
|
36dd5d1533 | ||
|
|
33b9bff0b9 | ||
|
|
c24d6338e2 | ||
|
|
8cb6fc4e78 | ||
|
|
a9dfff84e6 | ||
|
|
988a91634a | ||
|
|
8f6df3b455 | ||
|
|
ef47fa4c74 | ||
|
|
f2f560a345 | ||
|
|
6a0ee68692 | ||
|
|
72741b90f4 | ||
|
|
0296ab60c3 | ||
|
|
77a53347c3 | ||
|
|
c32ea286f2 | ||
|
|
b2fb857f5b | ||
|
|
20cdabc8a4 | ||
|
|
e2f4c193e3 | ||
|
|
b4aa9593dc | ||
|
|
20c729893b | ||
|
|
a00f49331c | ||
|
|
85308e48fd | ||
|
|
ff08a0fb25 | ||
|
|
6145b69c97 | ||
|
|
cfc5ae1b94 | ||
|
|
54b221aabd | ||
|
|
dabee9d47a | ||
|
|
e30520d9f3 | ||
|
|
8f0acb2176 | ||
|
|
8edeff8160 | ||
|
|
cacec0d25c | ||
|
|
c54d7da19f | ||
|
|
df610e1045 | ||
|
|
8d4e6a13b0 | ||
|
|
c852e5f827 | ||
|
|
2a37f1650b | ||
|
|
15f160c874 | ||
|
|
df63ad4678 | ||
|
|
72cd61cccf | ||
|
|
3e5a444023 | ||
|
|
a3f4294cd8 | ||
|
|
d9401b70f5 | ||
|
|
4296d5dada | ||
|
|
12937b93d5 | ||
|
|
28cd5e5b87 | ||
|
|
7bacef7622 | ||
|
|
ca3638313b | ||
|
|
bb3e4ec2c7 | ||
|
|
d52ac784f6 | ||
|
|
f5a9214dd4 | ||
|
|
5b54a8dbe2 | ||
|
|
32165e97ed | ||
|
|
4b6f65881d | ||
|
|
0e7d8da407 | ||
|
|
1c862b21e9 | ||
|
|
b202438a4d | ||
|
|
50ee2ba964 | ||
|
|
9f40713ee1 | ||
|
|
5f393ae972 | ||
|
|
aca3ee9e23 | ||
|
|
284b17d1db | ||
|
|
506a4496d1 | ||
|
|
b5c201d1c1 | ||
|
|
032faa9882 | ||
|
|
414baadc40 | ||
|
|
2d6716aa47 | ||
|
|
260619923a | ||
|
|
3715c89ec8 | ||
|
|
5026108079 | ||
|
|
5f7c556697 | ||
|
|
3c7564da19 | ||
|
|
f0e070be5b | ||
|
|
aa05c470b3 | ||
|
|
13af11a3a6 | ||
|
|
41330940c6 | ||
|
|
46e630d2f6 | ||
|
|
63d87f0526 | ||
|
|
b578e9897e | ||
|
|
b2ebcf99c8 | ||
|
|
6a6b910869 | ||
|
|
4e8acf371f | ||
|
|
c4cb8288c7 | ||
|
|
2dddb879bc | ||
|
|
4d92dc662c | ||
|
|
3cb649d97c | ||
|
|
d9b5f2b03d | ||
|
|
2997906d98 | ||
|
|
bcd3deaa13 |
@@ -1,39 +0,0 @@
|
||||
[bumpversion]
|
||||
commit = True
|
||||
tag = True
|
||||
current_version = 5.46
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)
|
||||
serialize =
|
||||
{major}.{minor}
|
||||
|
||||
[bumpversion:file:src/libs/const.h]
|
||||
parse = (?P<major>\d+)
|
||||
serialize = {major}
|
||||
search = VERSION_MAJOR {current_version}
|
||||
replace = VERSION_MAJOR {new_version}
|
||||
|
||||
[bumpversion:file:./src/libs/const.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}
|
||||
replace = pkgver={new_version}
|
||||
|
||||
[bumpversion:file:pkg/openwrt/Makefile]
|
||||
search = PKG_VERSION:={current_version}
|
||||
replace = PKG_VERSION:={new_version}
|
||||
|
||||
[bumpversion:file:man/ustreamer.1]
|
||||
search = "version {current_version}"
|
||||
replace = "version {new_version}"
|
||||
|
||||
[bumpversion:file:man/ustreamer-dump.1]
|
||||
search = "version {current_version}"
|
||||
replace = "version {new_version}"
|
||||
2
.github/workflows/docker-alpine-image.yaml
vendored
2
.github/workflows/docker-alpine-image.yaml
vendored
@@ -66,7 +66,7 @@ jobs:
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/arm64,linux/amd64,linux/arm/v7
|
||||
platforms: linux/arm64,linux/amd64,linux/arm/v6,linux/arm/v7
|
||||
file: pkg/docker/Dockerfile.alpine
|
||||
-
|
||||
name: Push
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -3,11 +3,13 @@
|
||||
/pkg/arch/pkg/
|
||||
/pkg/arch/src/
|
||||
/src/build/
|
||||
/python/build/
|
||||
/python/dist/
|
||||
/python/ustreamer.egg-info/
|
||||
/python/root/
|
||||
/python/build/
|
||||
/janus/build/
|
||||
/ustreamer
|
||||
/ustreamer-dump
|
||||
/ustreamer-*
|
||||
/config.mk
|
||||
vgcore.*
|
||||
*.sock
|
||||
|
||||
71
Makefile
71
Makefile
@@ -1,16 +1,39 @@
|
||||
-include config.mk
|
||||
|
||||
|
||||
# =====
|
||||
DESTDIR ?=
|
||||
PREFIX ?= /usr/local
|
||||
MANPREFIX ?= $(PREFIX)/share/man
|
||||
|
||||
CC ?= gcc
|
||||
PY ?= python3
|
||||
PKG_CONFIG ?= pkg-config
|
||||
CFLAGS ?= -O3
|
||||
LDFLAGS ?=
|
||||
|
||||
RPI_VC_HEADERS ?= /opt/vc/include
|
||||
RPI_VC_LIBS ?= /opt/vc/lib
|
||||
R_DESTDIR = $(if $(DESTDIR),$(shell realpath "$(DESTDIR)"),)
|
||||
|
||||
WITH_PYTHON ?= 0
|
||||
WITH_JANUS ?= 0
|
||||
WITH_V4P ?= 0
|
||||
WITH_GPIO ?= 0
|
||||
WITH_SYSTEMD ?= 0
|
||||
WITH_PTHREAD_NP ?= 1
|
||||
WITH_SETPROCTITLE ?= 1
|
||||
WITH_PDEATHSIG ?= 1
|
||||
|
||||
define optbool
|
||||
$(filter $(shell echo $(1) | tr A-Z a-z), yes on 1)
|
||||
endef
|
||||
MK_WITH_PYTHON = $(call optbool,$(WITH_PYTHON))
|
||||
MK_WITH_JANUS = $(call optbool,$(WITH_JANUS))
|
||||
MK_WITH_V4P = $(call optbool,$(WITH_V4P))
|
||||
MK_WITH_GPIO = $(call optbool,$(WITH_GPIO))
|
||||
MK_WITH_SYSTEMD = $(call optbool,$(WITH_SYSTEMD))
|
||||
MK_WITH_PTHREAD_NP = $(call optbool,$(WITH_PTHREAD_NP))
|
||||
MK_WITH_SETPROCTITLE = $(call optbool,$(WITH_SETPROCTITLE))
|
||||
MK_WITH_PDEATHSIG = $(call optbool,$(WITH_PDEATHSIG))
|
||||
|
||||
export
|
||||
|
||||
@@ -18,50 +41,57 @@ _LINTERS_IMAGE ?= ustreamer-linters
|
||||
|
||||
|
||||
# =====
|
||||
define optbool
|
||||
$(filter $(shell echo $(1) | tr A-Z a-z), yes on 1)
|
||||
endef
|
||||
ifeq (__not_found__,$(shell which $(PKG_CONFIG) 2>/dev/null || echo "__not_found__"))
|
||||
$(error "No $(PKG_CONFIG) found in $(PATH)")
|
||||
endif
|
||||
|
||||
|
||||
# =====
|
||||
ifeq ($(V),)
|
||||
ECHO = @
|
||||
endif
|
||||
|
||||
|
||||
# =====
|
||||
all:
|
||||
+ $(MAKE) apps
|
||||
ifneq ($(call optbool,$(WITH_PYTHON)),)
|
||||
ifneq ($(MK_WITH_PYTHON),)
|
||||
+ $(MAKE) python
|
||||
endif
|
||||
ifneq ($(call optbool,$(WITH_JANUS)),)
|
||||
ifneq ($(MK_WITH_JANUS),)
|
||||
+ $(MAKE) janus
|
||||
endif
|
||||
|
||||
|
||||
apps:
|
||||
$(MAKE) -C src
|
||||
@ ln -sf src/ustreamer.bin ustreamer
|
||||
@ ln -sf src/ustreamer-dump.bin ustreamer-dump
|
||||
for i in src/*.bin; do \
|
||||
test ! -x $$i || ln -sf $$i `basename $$i .bin`; \
|
||||
done
|
||||
|
||||
|
||||
python:
|
||||
$(MAKE) -C python
|
||||
@ ln -sf python/build/lib.*/*.so .
|
||||
$(ECHO) ln -sf python/build/lib.linux-*/*.so -t .
|
||||
|
||||
|
||||
janus:
|
||||
$(MAKE) -C janus
|
||||
@ ln -sf janus/*.so .
|
||||
$(ECHO) ln -sf janus/*.so .
|
||||
|
||||
|
||||
install: all
|
||||
$(MAKE) -C src install
|
||||
ifneq ($(call optbool,$(WITH_PYTHON)),)
|
||||
ifneq ($(MK_WITH_PYTHON),)
|
||||
$(MAKE) -C python install
|
||||
endif
|
||||
ifneq ($(call optbool,$(WITH_JANUS)),)
|
||||
ifneq ($(MK_WITH_JANUS),)
|
||||
$(MAKE) -C janus install
|
||||
endif
|
||||
mkdir -p $(DESTDIR)$(MANPREFIX)/man1
|
||||
mkdir -p $(R_DESTDIR)$(MANPREFIX)/man1
|
||||
for man in $(shell ls man); do \
|
||||
install -m644 man/$$man $(DESTDIR)$(MANPREFIX)/man1/$$man; \
|
||||
gzip -f $(DESTDIR)$(MANPREFIX)/man1/$$man; \
|
||||
install -m644 man/$$man $(R_DESTDIR)$(MANPREFIX)/man1/$$man; \
|
||||
gzip -f $(R_DESTDIR)$(MANPREFIX)/man1/$$man; \
|
||||
done
|
||||
|
||||
|
||||
@@ -70,9 +100,8 @@ install-strip: install
|
||||
|
||||
|
||||
regen:
|
||||
tools/$(MAKE)-jpeg-h.py src/ustreamer/data/blank.jpeg src/ustreamer/data/blank_jpeg.c BLANK
|
||||
tools/$(MAKE)-ico-h.py src/ustreamer/data/favicon.ico src/ustreamer/data/favicon_ico.c FAVICON
|
||||
tools/$(MAKE)-html-h.py src/ustreamer/data/index.html src/ustreamer/data/index_html.c INDEX
|
||||
tools/make-ico-h.py src/ustreamer/data/favicon.ico src/ustreamer/data/favicon_ico.c FAVICON
|
||||
tools/make-html-h.py src/ustreamer/data/index.html src/ustreamer/data/index_html.c INDEX
|
||||
|
||||
|
||||
release:
|
||||
@@ -103,7 +132,7 @@ linters:
|
||||
|
||||
|
||||
bump:
|
||||
bumpversion $(if $(V),$(V),minor)
|
||||
./bump.py $(if $(V),$(V),minor)
|
||||
|
||||
|
||||
push:
|
||||
@@ -119,7 +148,7 @@ clean-all: linters clean
|
||||
|
||||
clean:
|
||||
rm -rf pkg/arch/pkg pkg/arch/src pkg/arch/v*.tar.gz pkg/arch/ustreamer-*.pkg.tar.{xz,zst}
|
||||
rm -f ustreamer ustreamer-dump *.so
|
||||
rm -f ustreamer ustreamer-* *.so
|
||||
$(MAKE) -C src clean
|
||||
$(MAKE) -C python clean
|
||||
$(MAKE) -C janus clean
|
||||
|
||||
27
README.md
27
README.md
@@ -11,7 +11,7 @@
|
||||
|----------|---------------|-------------------|
|
||||
| Multithreaded JPEG encoding | ✔ | ✘ |
|
||||
| Hardware image encoding<br>on Raspberry Pi | ✔ | ✘ |
|
||||
| Behavior when the device<br>is disconnected while streaming | ✔ Shows a black screen<br>with ```NO SIGNAL``` on it<br>until reconnected | ✘ Stops the streaming <sup>1</sup> |
|
||||
| Behavior when the device<br>is disconnected while streaming | ✔ Shows a black screen<br>with ```NO LIVE VIDEO``` on it<br>until reconnected | ✘ Stops the streaming <sup>1</sup> |
|
||||
| [DV-timings](https://linuxtv.org/downloads/v4l-dvb-apis-new/userspace-api/v4l/dv-timings.html) support -<br>the ability to change resolution<br>on the fly by source signal | ✔ | ☹ Partially yes <sup>1</sup> |
|
||||
| Option to skip frames when streaming<br>static images by HTTP to save the traffic | ✔ <sup>2</sup> | ✘ |
|
||||
| Streaming via UNIX domain socket | ✔ | ✘ |
|
||||
@@ -23,7 +23,7 @@
|
||||
| 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.
|
||||
* ```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 on 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()```.
|
||||
|
||||
@@ -35,16 +35,17 @@ If you're going to live-stream from your backyard webcam and need to control it,
|
||||
# Installation
|
||||
|
||||
## Building
|
||||
You need to download the µStreamer onto your system and build it from the sources.
|
||||
You need to download the µStreamer onto your system and build it from the sources, or use a package:
|
||||
|
||||
* AUR has a package for Arch Linux: https://aur.archlinux.org/packages/ustreamer.
|
||||
* Fedora: https://src.fedoraproject.org/rpms/ustreamer.
|
||||
* Ubuntu: https://packages.ubuntu.com/jammy/ustreamer.
|
||||
* Arch Linux: https://aur.archlinux.org/packages/ustreamer
|
||||
* Fedora: https://src.fedoraproject.org/rpms/ustreamer
|
||||
* Ubuntu: https://packages.ubuntu.com/jammy/ustreamer
|
||||
* Debian: https://packages.debian.org/sid/ustreamer
|
||||
* FreeBSD port: https://www.freshports.org/multimedia/ustreamer.
|
||||
* OpenWRT: https://github.com/openwrt/packages/tree/master/multimedia/ustreamer
|
||||
* FreeBSD port: https://www.freshports.org/multimedia/ustreamer
|
||||
|
||||
### Preconditions
|
||||
You'll need ```make```, ```gcc```, ```libevent``` with ```pthreads``` support, ```libjpeg9```/```libjpeg-turbo``` and ```libbsd``` (only for Linux).
|
||||
You'll need ```make```, ```gcc```, ```pkg-config```, ```libevent``` with ```pthreads``` support, ```libjpeg9```/```libjpeg-turbo``` and ```libbsd``` (only for Linux).
|
||||
|
||||
* Arch: `sudo pacman -S libevent libjpeg-turbo libutil-linux libbsd`.
|
||||
* Raspberry OS Bullseye: `sudo apt install libevent-dev libjpeg62-turbo libbsd-dev`. Add `libgpiod-dev` for `WITH_GPIO=1` and `libsystemd-dev` for `WITH_SYSTEMD=1` and `libasound2-dev libspeex-dev libspeexdsp-dev libopus-dev` for `WITH_JANUS=1`.
|
||||
@@ -85,13 +86,13 @@ Without arguments, ```ustreamer``` will try to open ```/dev/video0``` with 640x4
|
||||
|
||||
:exclamation: Please note that since µStreamer v2.0 cross-domain requests were disabled by default for [security reasons](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). To enable the old behavior, use the option `--allow-origin=\*`.
|
||||
|
||||
The recommended way of running µStreamer with [Auvidea B101](https://www.raspberrypi.org/forums/viewtopic.php?f=38&t=120702&start=400#p1339178) on Raspberry Pi:
|
||||
The recommended way of running µStreamer with [TC358743-based capture device](https://www.raspberrypi.org/forums/viewtopic.php?f=38&t=120702&start=400#p1339178) on Raspberry Pi:
|
||||
```
|
||||
$ ./ustreamer \
|
||||
--format=uyvy \ # Device input format
|
||||
--encoder=m2m-image \ # Hardware encoding on V4L2 M2M driver
|
||||
--workers=3 \ # Workers number
|
||||
--persistent \ # Don't re-initialize device on timeout (for example when HDMI cable was disconnected)
|
||||
--persistent \ # Suppress repetitive signal source errors (for example when HDMI cable was disconnected)
|
||||
--dv-timings \ # Use DV-timings
|
||||
--drop-same-frames=30 # Save the traffic
|
||||
```
|
||||
@@ -151,7 +152,7 @@ Add `-e EDID=1` to set HDMI EDID before starting ustreamer. Use together with `-
|
||||
-----
|
||||
# Raspberry Pi Camera Example
|
||||
|
||||
Example usage for the Raspberry Pi v3 camera (required `libcamerify` which is located in `libcamera-tools` on Raspbian):
|
||||
Example usage for the Raspberry Pi v3 camera (required `libcamerify` which is located in `libcamera-tools` and `libcamera-v4l2` (install both) on Raspbian):
|
||||
```
|
||||
$ sudo modprobe bcm2835-v4l2
|
||||
$ libcamerify ./ustreamer --host :: --encoder=m2m-image
|
||||
@@ -180,7 +181,7 @@ $ modprobe bcm2835-v4l2 max_video_width=2592 max_video_height=1944
|
||||
µStreamer supports bandwidth-efficient streaming using [H.264 compression](https://en.wikipedia.org/wiki/Advanced_Video_Coding) and the Janus WebRTC server. See the [Janus integration guide](docs/h264.md) for full details.
|
||||
|
||||
## 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:
|
||||
When uStreamer is behind an Nginx proxy, its buffering behavior introduces latency into the video stream. It's possible to disable Nginx's buffering to eliminate the additional latency:
|
||||
|
||||
```nginx
|
||||
location /stream {
|
||||
@@ -209,7 +210,7 @@ v4l2 utilities provide the tools to manage USB webcam setting and information. S
|
||||
|
||||
-----
|
||||
# License
|
||||
Copyright (C) 2018-2023 by Maxim Devaev mdevaev@gmail.com
|
||||
Copyright (C) 2018-2024 by 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
|
||||
|
||||
85
bump.py
Executable file
85
bump.py
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
|
||||
# =====
|
||||
def _run(cmd: list[str]) -> str:
|
||||
return str(subprocess.run(cmd, stdout=subprocess.PIPE, check=True).stdout.decode())
|
||||
|
||||
|
||||
def _get_version() -> tuple[int, int]:
|
||||
ver = _run(["/usr/bin/git", "describe", "--tags", "--abbrev=0"]).strip()
|
||||
assert (m := re.match(r"v(\d+)\.(\d+)", ver)), ver
|
||||
return (int(m.group(1)), int(m.group(2)))
|
||||
|
||||
|
||||
def _make_version(major: int, minor: int) -> str:
|
||||
return f"v{major}.{minor}"
|
||||
|
||||
|
||||
def _patch_file(path: str, replace: str, old: tuple[int, int], new: tuple[int, int]) -> None:
|
||||
with open(path, "r+") as f:
|
||||
old_text = f.read()
|
||||
new_text = old_text.replace(
|
||||
replace.format(major=old[0], minor=old[1]),
|
||||
replace.format(major=new[0], minor=new[1]),
|
||||
)
|
||||
# assert old_text != new_text, path
|
||||
f.seek(0, os.SEEK_SET)
|
||||
f.write(new_text)
|
||||
|
||||
|
||||
def _bump(do_major: bool, items: list[tuple[str, str]]) -> None:
|
||||
_run(["/usr/bin/git", "diff-index", "--quiet", "HEAD", "--"])
|
||||
|
||||
old = _get_version()
|
||||
new = ((old[0] + 1, old[1]) if do_major else (old[0], old[1] + 1))
|
||||
|
||||
for (path, replace) in items:
|
||||
_patch_file(path, replace, old, new)
|
||||
_run(["/usr/bin/git", "add", path])
|
||||
|
||||
ver_new = _make_version(*new)
|
||||
_run(["/usr/bin/git", "commit", "-m", f"Bump version: {_make_version(*old)} -> {ver_new}"])
|
||||
_run(["/usr/bin/git", "tag", ver_new])
|
||||
|
||||
|
||||
# =====
|
||||
if __name__ == "__main__":
|
||||
_bump(
|
||||
do_major=(len(sys.argv) == 2 and sys.argv[1] == "major"),
|
||||
items=[
|
||||
("src/libs/const.h", "define US_VERSION_MAJOR {major}"),
|
||||
("src/libs/const.h", "define US_VERSION_MINOR {minor}"),
|
||||
("python/setup.py", "version=\"{major}.{minor}\""),
|
||||
("man/ustreamer.1", "version {major}.{minor}"),
|
||||
("man/ustreamer-dump.1", "version {major}.{minor}"),
|
||||
("pkg/arch/PKGBUILD", "pkgver={major}.{minor}"),
|
||||
("pkg/openwrt/Makefile", "PKG_VERSION:={major}.{minor}"),
|
||||
],
|
||||
)
|
||||
@@ -1,7 +1,8 @@
|
||||
DESTDIR ?=
|
||||
R_DESTDIR ?=
|
||||
PREFIX ?= /usr/local
|
||||
|
||||
CC ?= gcc
|
||||
PKG_CONFIG ?= pkg-config
|
||||
CFLAGS ?= -O3
|
||||
LDFLAGS ?=
|
||||
|
||||
@@ -9,21 +10,20 @@ LDFLAGS ?=
|
||||
# =====
|
||||
_PLUGIN = libjanus_ustreamer.so
|
||||
|
||||
_CFLAGS = -fPIC -MD -c -std=c17 -Wall -Wextra -D_GNU_SOURCE $(shell pkg-config --cflags glib-2.0) $(CFLAGS)
|
||||
_LDFLAGS = -shared -lm -pthread -lrt -ljansson -lopus -lasound -lspeexdsp $(shell pkg-config --libs glib-2.0) $(LDFLAGS)
|
||||
_CFLAGS = -fPIC -MD -c -std=c17 -Wall -Wextra -D_GNU_SOURCE $(shell $(PKG_CONFIG) --cflags janus-gateway) $(CFLAGS)
|
||||
_LDFLAGS = -shared -lm -pthread -lrt -ljansson -lopus -lasound -lspeexdsp $(shell $(PKG_CONFIG) --libs janus-gateway) $(LDFLAGS)
|
||||
|
||||
_SRCS = $(shell ls src/uslibs/*.c src/*.c)
|
||||
|
||||
_BUILD = build
|
||||
|
||||
|
||||
define optbool
|
||||
$(filter $(shell echo $(1) | tr A-Z a-z), yes on 1)
|
||||
endef
|
||||
# =====
|
||||
ifeq ($(findstring bsd,$(shell $(CC) -dumpmachine)),)
|
||||
override _LDFLAGS += -latomic
|
||||
endif
|
||||
|
||||
|
||||
WITH_PTHREAD_NP ?= 1
|
||||
ifneq ($(call optbool,$(WITH_PTHREAD_NP)),)
|
||||
ifneq ($(MK_WITH_PTHREAD_NP),)
|
||||
override _CFLAGS += -DWITH_PTHREAD_NP
|
||||
endif
|
||||
|
||||
@@ -31,19 +31,19 @@ endif
|
||||
# =====
|
||||
$(_PLUGIN): $(_SRCS:%.c=$(_BUILD)/%.o)
|
||||
$(info == SO $@)
|
||||
@ $(CC) $^ -o $@ $(_LDFLAGS)
|
||||
$(ECHO) $(CC) $^ -o $@ $(_LDFLAGS)
|
||||
|
||||
|
||||
$(_BUILD)/%.o: %.c
|
||||
$(info -- CC $<)
|
||||
@ mkdir -p $(dir $@) || true
|
||||
@ $(CC) $< -o $@ $(_CFLAGS)
|
||||
$(ECHO) mkdir -p $(dir $@) || true
|
||||
$(ECHO) $(CC) $< -o $@ $(_CFLAGS)
|
||||
|
||||
|
||||
|
||||
install: $(_PLUGIN)
|
||||
mkdir -p $(DESTDIR)$(PREFIX)/lib/ustreamer/janus
|
||||
install -m755 $(_PLUGIN) $(DESTDIR)$(PREFIX)/lib/ustreamer/janus/$(PLUGIN)
|
||||
mkdir -p $(R_DESTDIR)$(PREFIX)/lib/ustreamer/janus
|
||||
install -m755 $(_PLUGIN) $(R_DESTDIR)$(PREFIX)/lib/ustreamer/janus/$(PLUGIN)
|
||||
|
||||
|
||||
clean:
|
||||
|
||||
242
janus/src/acap.c
Normal file
242
janus/src/acap.c
Normal file
@@ -0,0 +1,242 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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 "acap.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
#include <pthread.h>
|
||||
#include <alsa/asoundlib.h>
|
||||
#include <speex/speex_resampler.h>
|
||||
#include <opus/opus.h>
|
||||
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/errors.h"
|
||||
#include "uslibs/tools.h"
|
||||
#include "uslibs/threading.h"
|
||||
#include "uslibs/logging.h"
|
||||
#include "uslibs/array.h"
|
||||
#include "uslibs/ring.h"
|
||||
|
||||
#include "rtp.h"
|
||||
#include "au.h"
|
||||
|
||||
|
||||
static void *_pcm_thread(void *v_acap);
|
||||
static void *_encoder_thread(void *v_acap);
|
||||
|
||||
|
||||
us_acap_s *us_acap_init(const char *name, uint pcm_hz) {
|
||||
us_acap_s *acap;
|
||||
US_CALLOC(acap, 1);
|
||||
acap->pcm_hz = pcm_hz;
|
||||
US_RING_INIT_WITH_ITEMS(acap->pcm_ring, 8, us_au_pcm_init);
|
||||
US_RING_INIT_WITH_ITEMS(acap->enc_ring, 8, us_au_encoded_init);
|
||||
atomic_init(&acap->stop, false);
|
||||
|
||||
int err;
|
||||
|
||||
{
|
||||
if ((err = snd_pcm_open(&acap->dev, name, SND_PCM_STREAM_CAPTURE, 0)) < 0) {
|
||||
acap->dev = NULL;
|
||||
US_LOG_PERROR_ALSA(err, "Can't open PCM capture");
|
||||
goto error;
|
||||
}
|
||||
US_A(!snd_pcm_hw_params_malloc(&acap->dev_params));
|
||||
|
||||
# define SET_PARAM(_msg, _func, ...) { \
|
||||
if ((err = _func(acap->dev, acap->dev_params, ##__VA_ARGS__)) < 0) { \
|
||||
US_LOG_PERROR_ALSA(err, _msg); \
|
||||
goto error; \
|
||||
} \
|
||||
}
|
||||
|
||||
SET_PARAM("Can't initialize PCM params", snd_pcm_hw_params_any);
|
||||
SET_PARAM("Can't set PCM access type", snd_pcm_hw_params_set_access, SND_PCM_ACCESS_RW_INTERLEAVED);
|
||||
SET_PARAM("Can't set PCM channels number", snd_pcm_hw_params_set_channels, US_RTP_OPUS_CH);
|
||||
SET_PARAM("Can't set PCM sampling format", snd_pcm_hw_params_set_format, SND_PCM_FORMAT_S16_LE);
|
||||
SET_PARAM("Can't set PCM sampling rate", snd_pcm_hw_params_set_rate_near, &acap->pcm_hz, 0);
|
||||
if (acap->pcm_hz < US_AU_MIN_PCM_HZ || acap->pcm_hz > US_AU_MAX_PCM_HZ) {
|
||||
US_LOG_ERROR("Unsupported PCM freq: %u; should be: %u <= F <= %u",
|
||||
acap->pcm_hz, US_AU_MIN_PCM_HZ, US_AU_MAX_PCM_HZ);
|
||||
goto error;
|
||||
}
|
||||
acap->pcm_frames = US_AU_HZ_TO_FRAMES(acap->pcm_hz);
|
||||
acap->pcm_size = US_AU_HZ_TO_BUF8(acap->pcm_hz);
|
||||
SET_PARAM("Can't apply PCM params", snd_pcm_hw_params);
|
||||
|
||||
# undef SET_PARAM
|
||||
}
|
||||
|
||||
if (acap->pcm_hz != US_RTP_OPUS_HZ) {
|
||||
acap->res = speex_resampler_init(US_RTP_OPUS_CH, acap->pcm_hz, US_RTP_OPUS_HZ, SPEEX_RESAMPLER_QUALITY_DESKTOP, &err);
|
||||
if (err < 0) {
|
||||
acap->res = NULL;
|
||||
US_LOG_PERROR_RES(err, "Can't create resampler");
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// OPUS_APPLICATION_VOIP, OPUS_APPLICATION_RESTRICTED_LOWDELAY
|
||||
acap->enc = opus_encoder_create(US_RTP_OPUS_HZ, US_RTP_OPUS_CH, OPUS_APPLICATION_AUDIO, &err);
|
||||
US_A(err == 0);
|
||||
// https://github.com/meetecho/janus-gateway/blob/3cdd6ff/src/plugins/janus_audiobridge.c#L2272
|
||||
// https://datatracker.ietf.org/doc/html/rfc7587#section-3.1.1
|
||||
US_A(!opus_encoder_ctl(acap->enc, OPUS_SET_BITRATE(128000)));
|
||||
US_A(!opus_encoder_ctl(acap->enc, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_FULLBAND)));
|
||||
US_A(!opus_encoder_ctl(acap->enc, OPUS_SET_SIGNAL(OPUS_SIGNAL_MUSIC)));
|
||||
// OPUS_SET_INBAND_FEC(1), OPUS_SET_PACKET_LOSS_PERC(10): see rtpa.c
|
||||
}
|
||||
|
||||
US_LOG_INFO("Capture configured on %uHz; capturing ...", acap->pcm_hz);
|
||||
acap->tids_created = true;
|
||||
US_THREAD_CREATE(acap->enc_tid, _encoder_thread, acap);
|
||||
US_THREAD_CREATE(acap->pcm_tid, _pcm_thread, acap);
|
||||
|
||||
return acap;
|
||||
|
||||
error:
|
||||
us_acap_destroy(acap);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void us_acap_destroy(us_acap_s *acap) {
|
||||
if (acap->tids_created) {
|
||||
atomic_store(&acap->stop, true);
|
||||
US_THREAD_JOIN(acap->pcm_tid);
|
||||
US_THREAD_JOIN(acap->enc_tid);
|
||||
}
|
||||
US_DELETE(acap->enc, opus_encoder_destroy);
|
||||
US_DELETE(acap->res, speex_resampler_destroy);
|
||||
US_DELETE(acap->dev, snd_pcm_close);
|
||||
US_DELETE(acap->dev_params, snd_pcm_hw_params_free);
|
||||
US_RING_DELETE_WITH_ITEMS(acap->enc_ring, us_au_encoded_destroy);
|
||||
US_RING_DELETE_WITH_ITEMS(acap->pcm_ring, us_au_pcm_destroy);
|
||||
if (acap->tids_created) {
|
||||
US_LOG_INFO("Capture closed");
|
||||
}
|
||||
free(acap);
|
||||
}
|
||||
|
||||
int us_acap_get_encoded(us_acap_s *acap, u8 *data, uz *size, u64 *pts) {
|
||||
if (atomic_load(&acap->stop)) {
|
||||
return -1;
|
||||
}
|
||||
const int ri = us_ring_consumer_acquire(acap->enc_ring, 0.1);
|
||||
if (ri < 0) {
|
||||
return US_ERROR_NO_DATA;
|
||||
}
|
||||
const us_au_encoded_s *const buf = acap->enc_ring->items[ri];
|
||||
if (buf->used == 0 || *size < buf->used) {
|
||||
us_ring_consumer_release(acap->enc_ring, ri);
|
||||
return US_ERROR_NO_DATA;
|
||||
}
|
||||
memcpy(data, buf->data, buf->used);
|
||||
*size = buf->used;
|
||||
*pts = buf->pts;
|
||||
us_ring_consumer_release(acap->enc_ring, ri);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void *_pcm_thread(void *v_acap) {
|
||||
US_THREAD_SETTLE("us_acap_pcm");
|
||||
|
||||
us_acap_s *const acap = v_acap;
|
||||
u8 in[US_AU_MAX_BUF8];
|
||||
|
||||
while (!atomic_load(&acap->stop)) {
|
||||
const int frames = snd_pcm_readi(acap->dev, in, acap->pcm_frames);
|
||||
if (frames < 0) {
|
||||
US_LOG_PERROR_ALSA(frames, "Fatal: Can't capture PCM frames");
|
||||
break;
|
||||
} else if (frames < (int)acap->pcm_frames) {
|
||||
US_LOG_ERROR("Fatal: Too few PCM frames captured");
|
||||
break;
|
||||
}
|
||||
|
||||
const int ri = us_ring_producer_acquire(acap->pcm_ring, 0);
|
||||
if (ri >= 0) {
|
||||
us_au_pcm_s *const out = acap->pcm_ring->items[ri];
|
||||
memcpy(out->data, in, acap->pcm_size);
|
||||
us_ring_producer_release(acap->pcm_ring, ri);
|
||||
} else {
|
||||
US_LOG_ERROR("PCM ring is full");
|
||||
}
|
||||
}
|
||||
|
||||
atomic_store(&acap->stop, true);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void *_encoder_thread(void *v_acap) {
|
||||
US_THREAD_SETTLE("us_acap_enc");
|
||||
|
||||
us_acap_s *const acap = v_acap;
|
||||
s16 in_res[US_AU_MAX_BUF16];
|
||||
|
||||
while (!atomic_load(&acap->stop)) {
|
||||
const int in_ri = us_ring_consumer_acquire(acap->pcm_ring, 0.1);
|
||||
if (in_ri < 0) {
|
||||
continue;
|
||||
}
|
||||
us_au_pcm_s *const in = acap->pcm_ring->items[in_ri];
|
||||
|
||||
s16 *in_ptr;
|
||||
if (acap->res != NULL) {
|
||||
US_A(acap->pcm_hz != US_RTP_OPUS_HZ);
|
||||
u32 in_count = acap->pcm_frames;
|
||||
u32 out_count = US_AU_HZ_TO_FRAMES(US_RTP_OPUS_HZ);
|
||||
speex_resampler_process_interleaved_int(acap->res, in->data, &in_count, in_res, &out_count);
|
||||
in_ptr = in_res;
|
||||
} else {
|
||||
US_A(acap->pcm_hz == US_RTP_OPUS_HZ);
|
||||
in_ptr = in->data;
|
||||
}
|
||||
|
||||
const int out_ri = us_ring_producer_acquire(acap->enc_ring, 0);
|
||||
if (out_ri < 0) {
|
||||
US_LOG_ERROR("OPUS encoder queue is full");
|
||||
us_ring_consumer_release(acap->pcm_ring, in_ri);
|
||||
continue;
|
||||
}
|
||||
us_au_encoded_s *const out = acap->enc_ring->items[out_ri];
|
||||
|
||||
const int size = opus_encode(acap->enc, in_ptr, US_AU_HZ_TO_FRAMES(US_RTP_OPUS_HZ), out->data, US_ARRAY_LEN(out->data));
|
||||
us_ring_consumer_release(acap->pcm_ring, in_ri);
|
||||
|
||||
if (size > 0) {
|
||||
out->used = size;
|
||||
out->pts = acap->pts;
|
||||
// https://datatracker.ietf.org/doc/html/rfc7587#section-4.2
|
||||
acap->pts += US_AU_HZ_TO_FRAMES(US_RTP_OPUS_HZ);
|
||||
} else {
|
||||
out->used = 0;
|
||||
US_LOG_PERROR_OPUS(size, "Fatal: Can't encode PCM frame to OPUS");
|
||||
}
|
||||
us_ring_producer_release(acap->enc_ring, out_ri);
|
||||
}
|
||||
|
||||
atomic_store(&acap->stop, true);
|
||||
return NULL;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,28 +22,38 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdatomic.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "../libs/tools.h"
|
||||
#include "../libs/logging.h"
|
||||
#include "../libs/frame.h"
|
||||
#include "../libs/memsink.h"
|
||||
#include "../libs/unjpeg.h"
|
||||
#include "m2m.h"
|
||||
#include <pthread.h>
|
||||
#include <alsa/asoundlib.h>
|
||||
#include <speex/speex_resampler.h>
|
||||
#include <opus/opus.h>
|
||||
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/ring.h"
|
||||
|
||||
|
||||
typedef struct {
|
||||
us_memsink_s *sink;
|
||||
bool key_requested;
|
||||
us_frame_s *tmp_src;
|
||||
us_frame_s *dest;
|
||||
us_m2m_encoder_s *enc;
|
||||
atomic_bool online;
|
||||
} us_h264_stream_s;
|
||||
snd_pcm_t *dev;
|
||||
uint pcm_hz;
|
||||
uint pcm_frames;
|
||||
uz pcm_size;
|
||||
snd_pcm_hw_params_t *dev_params;
|
||||
SpeexResamplerState *res;
|
||||
OpusEncoder *enc;
|
||||
|
||||
us_ring_s *pcm_ring;
|
||||
us_ring_s *enc_ring;
|
||||
u32 pts;
|
||||
|
||||
pthread_t pcm_tid;
|
||||
pthread_t enc_tid;
|
||||
bool tids_created;
|
||||
atomic_bool stop;
|
||||
} us_acap_s;
|
||||
|
||||
|
||||
us_h264_stream_s *us_h264_stream_init(us_memsink_s *sink, const char *path, unsigned bitrate, unsigned gop);
|
||||
void us_h264_stream_destroy(us_h264_stream_s *h264);
|
||||
void us_h264_stream_process(us_h264_stream_s *h264, const us_frame_s *frame, bool force_key);
|
||||
us_acap_s *us_acap_init(const char *name, uint pcm_hz);
|
||||
void us_acap_destroy(us_acap_s *acap);
|
||||
|
||||
int us_acap_get_encoded(us_acap_s *acap, u8 *data, uz *size, u64 *pts);
|
||||
151
janus/src/au.c
Normal file
151
janus/src/au.c
Normal file
@@ -0,0 +1,151 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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 "au.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "uslibs/tools.h"
|
||||
|
||||
|
||||
bool us_au_probe(const char *name) {
|
||||
// This function is very limited. It takes something like:
|
||||
// hw:0,0 or hw:tc358743,0 or plughw:UAC2Gadget,0
|
||||
// parses card name (0, tc358743, UAC2Gadget) and checks
|
||||
// the existence of it in /proc/asound/.
|
||||
// It's enough for our case.
|
||||
|
||||
if (!us_str_is_ok(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strchr(name, '/') || strchr(name, '.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *begin = strchr(name, ':');
|
||||
if (begin == NULL) {
|
||||
return false;
|
||||
}
|
||||
begin += 1;
|
||||
if (*begin == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *end = strchr(begin, ',');
|
||||
if (end == NULL) {
|
||||
return false;
|
||||
}
|
||||
if (end - begin < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char *card = us_strdup(begin);
|
||||
card[end - begin] = '\0';
|
||||
|
||||
bool numeric = true;
|
||||
for (uz index = 0; card[index] != '\0'; ++index) {
|
||||
if (!isdigit(card[index])) {
|
||||
numeric = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
char *path;
|
||||
if (numeric) {
|
||||
US_ASPRINTF(path, "/proc/asound/card%s", card);
|
||||
} else {
|
||||
US_ASPRINTF(path, "/proc/asound/%s", card);
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
struct stat st;
|
||||
if (lstat(path, &st) == 0) {
|
||||
if (numeric && S_ISDIR(st.st_mode)) {
|
||||
ok = true;
|
||||
} else if (!numeric && S_ISLNK(st.st_mode)) {
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
free(path);
|
||||
free(card);
|
||||
return ok;
|
||||
}
|
||||
|
||||
us_au_pcm_s *us_au_pcm_init(void) {
|
||||
us_au_pcm_s *pcm;
|
||||
US_CALLOC(pcm, 1);
|
||||
return pcm;
|
||||
}
|
||||
|
||||
void us_au_pcm_destroy(us_au_pcm_s *pcm) {
|
||||
free(pcm);
|
||||
}
|
||||
|
||||
void us_au_pcm_mix(us_au_pcm_s *dest, us_au_pcm_s *src) {
|
||||
const uz size = src->frames * US_RTP_OPUS_CH * 2; // 2 for 16 bit
|
||||
|
||||
if (src->frames == 0) {
|
||||
return;
|
||||
|
||||
} else if (dest->frames == 0) {
|
||||
memcpy(dest->data, src->data, size);
|
||||
dest->frames = src->frames;
|
||||
|
||||
} else if (dest->frames == src->frames) {
|
||||
// https://stackoverflow.com/questions/12089662
|
||||
for (uz index = 0; index < size; ++index) {
|
||||
int a = dest->data[index];
|
||||
int b = src->data[index];
|
||||
int m;
|
||||
|
||||
a += 32768;
|
||||
b += 32768;
|
||||
|
||||
if ((a < 32768) && (b < 32768)) {
|
||||
m = a * b / 32768;
|
||||
} else {
|
||||
m = 2 * (a + b) - (a * b) / 32768 - 65536;
|
||||
}
|
||||
if (m == 65536) {
|
||||
m = 65535;
|
||||
}
|
||||
m -= 32768;
|
||||
|
||||
dest->data[index] = m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
us_au_encoded_s *us_au_encoded_init(void) {
|
||||
us_au_encoded_s *enc;
|
||||
US_CALLOC(enc, 1);
|
||||
return enc;
|
||||
}
|
||||
|
||||
void us_au_encoded_destroy(us_au_encoded_s *enc) {
|
||||
free(enc);
|
||||
}
|
||||
61
janus/src/au.h
Normal file
61
janus/src/au.h
Normal file
@@ -0,0 +1,61 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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 "uslibs/types.h"
|
||||
|
||||
#include "rtp.h"
|
||||
|
||||
// A number of frames per 1 channel:
|
||||
// - https://github.com/xiph/opus/blob/7b05f44/src/opus_demo.c#L368
|
||||
#define US_AU_FRAME_MS 20
|
||||
// #define _HZ_TO_FRAMES(_hz) (6 * (_hz) / 50) // 120ms
|
||||
#define US_AU_HZ_TO_FRAMES(_hz) ((_hz) / 50) // 20ms
|
||||
#define US_AU_HZ_TO_BUF16(_hz) (US_AU_HZ_TO_FRAMES(_hz) * US_RTP_OPUS_CH) // ... * 2: One stereo frame = (16bit L) + (16bit R)
|
||||
#define US_AU_HZ_TO_BUF8(_hz) (US_AU_HZ_TO_BUF16(_hz) * sizeof(s16))
|
||||
|
||||
#define US_AU_MIN_PCM_HZ 8000
|
||||
#define US_AU_MAX_PCM_HZ 192000
|
||||
#define US_AU_MAX_BUF16 US_AU_HZ_TO_BUF16(US_AU_MAX_PCM_HZ)
|
||||
#define US_AU_MAX_BUF8 US_AU_HZ_TO_BUF8(US_AU_MAX_PCM_HZ)
|
||||
|
||||
|
||||
typedef struct {
|
||||
s16 data[US_AU_MAX_BUF16];
|
||||
uz frames;
|
||||
} us_au_pcm_s;
|
||||
|
||||
typedef struct {
|
||||
u8 data[US_RTP_PAYLOAD_SIZE];
|
||||
uz used;
|
||||
u64 pts;
|
||||
} us_au_encoded_s;
|
||||
|
||||
bool us_au_probe(const char *name);
|
||||
|
||||
us_au_pcm_s *us_au_pcm_init(void);
|
||||
void us_au_pcm_destroy(us_au_pcm_s *pcm);
|
||||
void us_au_pcm_mix(us_au_pcm_s *a, us_au_pcm_s *b);
|
||||
|
||||
us_au_encoded_s *us_au_encoded_init(void);
|
||||
void us_au_encoded_destroy(us_au_encoded_s *enc);
|
||||
@@ -1,255 +0,0 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 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 "audio.h"
|
||||
|
||||
|
||||
#define _JLOG_PERROR_ALSA(_err, _prefix, _msg, ...) US_JLOG_ERROR(_prefix, _msg ": %s", ##__VA_ARGS__, snd_strerror(_err))
|
||||
#define _JLOG_PERROR_RES(_err, _prefix, _msg, ...) US_JLOG_ERROR(_prefix, _msg ": %s", ##__VA_ARGS__, speex_resampler_strerror(_err))
|
||||
#define _JLOG_PERROR_OPUS(_err, _prefix, _msg, ...) US_JLOG_ERROR(_prefix, _msg ": %s", ##__VA_ARGS__, opus_strerror(_err))
|
||||
|
||||
// A number of frames per 1 channel:
|
||||
// - https://github.com/xiph/opus/blob/7b05f44/src/opus_demo.c#L368
|
||||
#define _HZ_TO_FRAMES(_hz) (6 * (_hz) / 50) // 120ms
|
||||
#define _HZ_TO_BUF16(_hz) (_HZ_TO_FRAMES(_hz) * 2) // One stereo frame = (16bit L) + (16bit R)
|
||||
#define _HZ_TO_BUF8(_hz) (_HZ_TO_BUF16(_hz) * sizeof(int16_t))
|
||||
|
||||
#define _MIN_PCM_HZ 8000
|
||||
#define _MAX_PCM_HZ 192000
|
||||
#define _MAX_BUF16 _HZ_TO_BUF16(_MAX_PCM_HZ)
|
||||
#define _MAX_BUF8 _HZ_TO_BUF8(_MAX_PCM_HZ)
|
||||
#define _ENCODER_INPUT_HZ 48000
|
||||
|
||||
|
||||
typedef struct {
|
||||
int16_t data[_MAX_BUF16];
|
||||
} _pcm_buffer_s;
|
||||
|
||||
typedef struct {
|
||||
uint8_t data[_MAX_BUF8]; // Worst case
|
||||
size_t used;
|
||||
uint64_t pts;
|
||||
} _enc_buffer_s;
|
||||
|
||||
|
||||
static void *_pcm_thread(void *v_audio);
|
||||
static void *_encoder_thread(void *v_audio);
|
||||
|
||||
|
||||
bool us_audio_probe(const char *name) {
|
||||
snd_pcm_t *pcm;
|
||||
int err;
|
||||
US_JLOG_INFO("audio", "Probing PCM capture ...");
|
||||
if ((err = snd_pcm_open(&pcm, name, SND_PCM_STREAM_CAPTURE, 0)) < 0) {
|
||||
_JLOG_PERROR_ALSA(err, "audio", "Can't probe PCM capture");
|
||||
return false;
|
||||
}
|
||||
snd_pcm_close(pcm);
|
||||
US_JLOG_INFO("audio", "PCM capture is available");
|
||||
return true;
|
||||
}
|
||||
|
||||
us_audio_s *us_audio_init(const char *name, unsigned pcm_hz) {
|
||||
us_audio_s *audio;
|
||||
US_CALLOC(audio, 1);
|
||||
audio->pcm_hz = pcm_hz;
|
||||
audio->pcm_queue = us_queue_init(8);
|
||||
audio->enc_queue = us_queue_init(8);
|
||||
atomic_init(&audio->stop, false);
|
||||
|
||||
int err;
|
||||
|
||||
{
|
||||
if ((err = snd_pcm_open(&audio->pcm, name, SND_PCM_STREAM_CAPTURE, 0)) < 0) {
|
||||
audio->pcm = NULL;
|
||||
_JLOG_PERROR_ALSA(err, "audio", "Can't open PCM capture");
|
||||
goto error;
|
||||
}
|
||||
assert(!snd_pcm_hw_params_malloc(&audio->pcm_params));
|
||||
|
||||
# define SET_PARAM(_msg, _func, ...) { \
|
||||
if ((err = _func(audio->pcm, audio->pcm_params, ##__VA_ARGS__)) < 0) { \
|
||||
_JLOG_PERROR_ALSA(err, "audio", _msg); \
|
||||
goto error; \
|
||||
} \
|
||||
}
|
||||
|
||||
SET_PARAM("Can't initialize PCM params", snd_pcm_hw_params_any);
|
||||
SET_PARAM("Can't set PCM access type", snd_pcm_hw_params_set_access, SND_PCM_ACCESS_RW_INTERLEAVED);
|
||||
SET_PARAM("Can't set PCM channels numbre", snd_pcm_hw_params_set_channels, 2);
|
||||
SET_PARAM("Can't set PCM sampling format", snd_pcm_hw_params_set_format, SND_PCM_FORMAT_S16_LE);
|
||||
SET_PARAM("Can't set PCM sampling rate", snd_pcm_hw_params_set_rate_near, &audio->pcm_hz, 0);
|
||||
if (audio->pcm_hz < _MIN_PCM_HZ || audio->pcm_hz > _MAX_PCM_HZ) {
|
||||
US_JLOG_ERROR("audio", "Unsupported PCM freq: %u; should be: %u <= F <= %u",
|
||||
audio->pcm_hz, _MIN_PCM_HZ, _MAX_PCM_HZ);
|
||||
goto error;
|
||||
}
|
||||
audio->pcm_frames = _HZ_TO_FRAMES(audio->pcm_hz);
|
||||
audio->pcm_size = _HZ_TO_BUF8(audio->pcm_hz);
|
||||
SET_PARAM("Can't apply PCM params", snd_pcm_hw_params);
|
||||
|
||||
# undef SET_PARAM
|
||||
}
|
||||
|
||||
if (audio->pcm_hz != _ENCODER_INPUT_HZ) {
|
||||
audio->res = speex_resampler_init(2, audio->pcm_hz, _ENCODER_INPUT_HZ, SPEEX_RESAMPLER_QUALITY_DESKTOP, &err);
|
||||
if (err < 0) {
|
||||
audio->res = NULL;
|
||||
_JLOG_PERROR_RES(err, "audio", "Can't create resampler");
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// OPUS_APPLICATION_VOIP, OPUS_APPLICATION_RESTRICTED_LOWDELAY
|
||||
audio->enc = opus_encoder_create(_ENCODER_INPUT_HZ, 2, OPUS_APPLICATION_AUDIO, &err);
|
||||
assert(err == 0);
|
||||
assert(!opus_encoder_ctl(audio->enc, OPUS_SET_BITRATE(48000)));
|
||||
assert(!opus_encoder_ctl(audio->enc, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_FULLBAND)));
|
||||
assert(!opus_encoder_ctl(audio->enc, OPUS_SET_SIGNAL(OPUS_SIGNAL_MUSIC)));
|
||||
// OPUS_SET_INBAND_FEC(1), OPUS_SET_PACKET_LOSS_PERC(10): see rtpa.c
|
||||
}
|
||||
|
||||
US_JLOG_INFO("audio", "Pipeline configured on %uHz; capturing ...", audio->pcm_hz);
|
||||
audio->tids_created = true;
|
||||
US_THREAD_CREATE(audio->enc_tid, _encoder_thread, audio);
|
||||
US_THREAD_CREATE(audio->pcm_tid, _pcm_thread, audio);
|
||||
|
||||
return audio;
|
||||
|
||||
error:
|
||||
us_audio_destroy(audio);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void us_audio_destroy(us_audio_s *audio) {
|
||||
if (audio->tids_created) {
|
||||
atomic_store(&audio->stop, true);
|
||||
US_THREAD_JOIN(audio->pcm_tid);
|
||||
US_THREAD_JOIN(audio->enc_tid);
|
||||
}
|
||||
US_DELETE(audio->enc, opus_encoder_destroy);
|
||||
US_DELETE(audio->res, speex_resampler_destroy);
|
||||
US_DELETE(audio->pcm, snd_pcm_close);
|
||||
US_DELETE(audio->pcm_params, snd_pcm_hw_params_free);
|
||||
US_QUEUE_DELETE_WITH_ITEMS(audio->enc_queue, free);
|
||||
US_QUEUE_DELETE_WITH_ITEMS(audio->pcm_queue, free);
|
||||
if (audio->tids_created) {
|
||||
US_JLOG_INFO("audio", "Pipeline closed");
|
||||
}
|
||||
free(audio);
|
||||
}
|
||||
|
||||
int us_audio_get_encoded(us_audio_s *audio, uint8_t *data, size_t *size, uint64_t *pts) {
|
||||
if (atomic_load(&audio->stop)) {
|
||||
return -1;
|
||||
}
|
||||
_enc_buffer_s *buf;
|
||||
if (!us_queue_get(audio->enc_queue, (void **)&buf, 0.1)) {
|
||||
if (*size < buf->used) {
|
||||
free(buf);
|
||||
return -3;
|
||||
}
|
||||
memcpy(data, buf->data, buf->used);
|
||||
*size = buf->used;
|
||||
*pts = buf->pts;
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
return -2;
|
||||
}
|
||||
|
||||
static void *_pcm_thread(void *v_audio) {
|
||||
US_THREAD_RENAME("us_a_pcm");
|
||||
|
||||
us_audio_s *const audio = (us_audio_s *)v_audio;
|
||||
uint8_t in[_MAX_BUF8];
|
||||
|
||||
while (!atomic_load(&audio->stop)) {
|
||||
const int frames = snd_pcm_readi(audio->pcm, in, audio->pcm_frames);
|
||||
if (frames < 0) {
|
||||
_JLOG_PERROR_ALSA(frames, "audio", "Fatal: Can't capture PCM frames");
|
||||
break;
|
||||
} else if (frames < (int)audio->pcm_frames) {
|
||||
US_JLOG_ERROR("audio", "Fatal: Too few PCM frames captured");
|
||||
break;
|
||||
}
|
||||
|
||||
if (us_queue_get_free(audio->pcm_queue)) {
|
||||
_pcm_buffer_s *out;
|
||||
US_CALLOC(out, 1);
|
||||
memcpy(out->data, in, audio->pcm_size);
|
||||
assert(!us_queue_put(audio->pcm_queue, out, 0));
|
||||
} else {
|
||||
US_JLOG_ERROR("audio", "PCM queue is full");
|
||||
}
|
||||
}
|
||||
|
||||
atomic_store(&audio->stop, true);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void *_encoder_thread(void *v_audio) {
|
||||
US_THREAD_RENAME("us_a_enc");
|
||||
|
||||
us_audio_s *const audio = (us_audio_s *)v_audio;
|
||||
int16_t in_res[_MAX_BUF16];
|
||||
|
||||
while (!atomic_load(&audio->stop)) {
|
||||
_pcm_buffer_s *in;
|
||||
if (!us_queue_get(audio->pcm_queue, (void **)&in, 0.1)) {
|
||||
int16_t *in_ptr;
|
||||
if (audio->res != NULL) {
|
||||
assert(audio->pcm_hz != _ENCODER_INPUT_HZ);
|
||||
uint32_t in_count = audio->pcm_frames;
|
||||
uint32_t out_count = _HZ_TO_FRAMES(_ENCODER_INPUT_HZ);
|
||||
speex_resampler_process_interleaved_int(audio->res, in->data, &in_count, in_res, &out_count);
|
||||
in_ptr = in_res;
|
||||
} else {
|
||||
assert(audio->pcm_hz == _ENCODER_INPUT_HZ);
|
||||
in_ptr = in->data;
|
||||
}
|
||||
|
||||
_enc_buffer_s *out;
|
||||
US_CALLOC(out, 1);
|
||||
const int size = opus_encode(audio->enc, in_ptr, _HZ_TO_FRAMES(_ENCODER_INPUT_HZ), out->data, US_ARRAY_LEN(out->data));
|
||||
free(in);
|
||||
if (size < 0) {
|
||||
_JLOG_PERROR_OPUS(size, "audio", "Fatal: Can't encode PCM frame to OPUS");
|
||||
free(out);
|
||||
break;
|
||||
}
|
||||
out->used = size;
|
||||
out->pts = audio->pts;
|
||||
// https://datatracker.ietf.org/doc/html/rfc7587#section-4.2
|
||||
audio->pts += _HZ_TO_FRAMES(_ENCODER_INPUT_HZ);
|
||||
|
||||
if (us_queue_put(audio->enc_queue, out, 0) != 0) {
|
||||
US_JLOG_ERROR("audio", "OPUS encoder queue is full");
|
||||
free(out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
atomic_store(&audio->stop, true);
|
||||
return NULL;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,10 +22,34 @@
|
||||
|
||||
#include "client.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdatomic.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <pthread.h>
|
||||
#include <janus/plugins/plugin.h>
|
||||
#include <janus/rtp.h>
|
||||
#include <opus/opus.h>
|
||||
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/tools.h"
|
||||
#include "uslibs/threading.h"
|
||||
#include "uslibs/logging.h"
|
||||
#include "uslibs/array.h"
|
||||
#include "uslibs/list.h"
|
||||
#include "uslibs/ring.h"
|
||||
#include "uslibs/frame.h"
|
||||
|
||||
#include "au.h"
|
||||
#include "rtp.h"
|
||||
|
||||
|
||||
static us_frame_s *_add_vplay_frame_to_ring(us_frame_s *frame, void *v_client);
|
||||
|
||||
static void *_video_thread(void *v_client);
|
||||
static void *_audio_thread(void *v_client);
|
||||
static void *_common_thread(void *v_client, bool video);
|
||||
static void *_acap_thread(void *v_client);
|
||||
static void *_video_or_acap_thread(void *v_client, bool video);
|
||||
static void *_aplay_thread(void *v_client);
|
||||
|
||||
|
||||
us_janus_client_s *us_janus_client_init(janus_callbacks *gw, janus_plugin_session *session) {
|
||||
@@ -33,30 +57,47 @@ us_janus_client_s *us_janus_client_init(janus_callbacks *gw, janus_plugin_sessio
|
||||
US_CALLOC(client, 1);
|
||||
client->gw = gw;
|
||||
client->session = session;
|
||||
client->video_ssrc = us_get_ssrc();
|
||||
client->audio_ssrc = us_get_ssrc();
|
||||
|
||||
atomic_init(&client->transmit, false);
|
||||
atomic_init(&client->transmit_audio, false);
|
||||
atomic_init(&client->transmit_acap, false);
|
||||
atomic_init(&client->transmit_aplay, false);
|
||||
atomic_init(&client->transmit_vplay, false);
|
||||
atomic_init(&client->video_orient, 0);
|
||||
|
||||
atomic_init(&client->stop, false);
|
||||
|
||||
client->video_queue = us_queue_init(2048);
|
||||
US_RING_INIT_WITH_ITEMS(client->video_ring, 2048, us_rtp_init);
|
||||
US_THREAD_CREATE(client->video_tid, _video_thread, client);
|
||||
|
||||
client->audio_queue = us_queue_init(64);
|
||||
US_THREAD_CREATE(client->audio_tid, _audio_thread, client);
|
||||
US_RING_INIT_WITH_ITEMS(client->acap_ring, 64, us_rtp_init);
|
||||
US_THREAD_CREATE(client->acap_tid, _acap_thread, client);
|
||||
|
||||
US_RING_INIT_WITH_ITEMS(client->aplay_enc_ring, 64, us_au_encoded_init);
|
||||
US_RING_INIT_WITH_ITEMS(client->aplay_pcm_ring, 64, us_au_pcm_init);
|
||||
US_THREAD_CREATE(client->aplay_tid, _aplay_thread, client);
|
||||
|
||||
client->rtpc = us_rtpc_init(_add_vplay_frame_to_ring, client);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
void us_janus_client_destroy(us_janus_client_s *client) {
|
||||
atomic_store(&client->stop, true);
|
||||
us_queue_put(client->video_queue, NULL, 0);
|
||||
us_queue_put(client->audio_queue, NULL, 0);
|
||||
|
||||
US_THREAD_JOIN(client->video_tid);
|
||||
US_QUEUE_DELETE_WITH_ITEMS(client->video_queue, us_rtp_destroy);
|
||||
US_RING_DELETE_WITH_ITEMS(client->video_ring, us_rtp_destroy);
|
||||
|
||||
US_THREAD_JOIN(client->audio_tid);
|
||||
US_QUEUE_DELETE_WITH_ITEMS(client->audio_queue, us_rtp_destroy);
|
||||
US_THREAD_JOIN(client->acap_tid);
|
||||
US_RING_DELETE_WITH_ITEMS(client->acap_ring, us_rtp_destroy);
|
||||
|
||||
US_THREAD_JOIN(client->aplay_tid);
|
||||
US_RING_DELETE_WITH_ITEMS(client->aplay_enc_ring, us_au_encoded_destroy);
|
||||
US_RING_DELETE_WITH_ITEMS(client->aplay_pcm_ring, us_au_pcm_destroy);
|
||||
|
||||
us_rtpc_destroy(client->rtpc);
|
||||
// us_janus_client_stop_vplay(client);
|
||||
|
||||
free(client);
|
||||
}
|
||||
@@ -64,67 +105,234 @@ void us_janus_client_destroy(us_janus_client_s *client) {
|
||||
void us_janus_client_send(us_janus_client_s *client, const us_rtp_s *rtp) {
|
||||
if (
|
||||
atomic_load(&client->transmit)
|
||||
&& (rtp->video || atomic_load(&client->transmit_audio))
|
||||
&& (rtp->video || atomic_load(&client->transmit_acap))
|
||||
) {
|
||||
us_rtp_s *const new = us_rtp_dup(rtp);
|
||||
if (us_queue_put((new->video ? client->video_queue : client->audio_queue), new, 0) != 0) {
|
||||
US_JLOG_ERROR("client", "Session %p %s queue is full",
|
||||
client->session, (new->video ? "video" : "audio"));
|
||||
us_rtp_destroy(new);
|
||||
us_ring_s *const ring = (rtp->video ? client->video_ring : client->acap_ring);
|
||||
const int ri = us_ring_producer_acquire(ring, 0);
|
||||
if (ri < 0) {
|
||||
US_LOG_ERROR("Session %p %s ring is full",
|
||||
client->session, (rtp->video ? "video" : "acap"));
|
||||
return;
|
||||
}
|
||||
memcpy(ring->items[ri], rtp, sizeof(us_rtp_s));
|
||||
us_ring_producer_release(ring, ri);
|
||||
}
|
||||
}
|
||||
|
||||
static us_frame_s *_add_vplay_frame_to_ring(us_frame_s *ready_frame, void *v_client) {
|
||||
us_janus_client_s *const client = v_client;
|
||||
|
||||
us_ring_s *const ring = client->vplay_enc_ring;
|
||||
if (!ring) {
|
||||
US_LOG_ERROR("Session %p has no vplay ring", client->session);
|
||||
return ready_frame;
|
||||
}
|
||||
|
||||
const int ri = us_ring_producer_acquire(ring, 0);
|
||||
if (ri < 0) {
|
||||
US_LOG_ERROR("Session %p vplay ring is full", client->session);
|
||||
return ready_frame;
|
||||
}
|
||||
|
||||
us_frame_s *const next_frame = ring->items[ri];
|
||||
ring->items[ri] = ready_frame;
|
||||
us_ring_producer_release(ring, ri);
|
||||
|
||||
return next_frame;
|
||||
}
|
||||
|
||||
void us_janus_client_recv(us_janus_client_s *client, janus_plugin_rtp *packet) {
|
||||
if (packet->length < sizeof(janus_rtp_header) || !atomic_load(&client->transmit)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int payload_size = 0;
|
||||
const u8 *const payload = (u8 *)janus_rtp_payload(packet->buffer, packet->length, &payload_size);
|
||||
if (payload == NULL || payload_size < 1) {
|
||||
return;
|
||||
}
|
||||
const janus_rtp_header *const header = (janus_rtp_header*)packet->buffer;
|
||||
const u16 seq = ntohs(header->seq_number);
|
||||
|
||||
if (packet->video) {
|
||||
if (header->type == US_RTP_H264_PAYLOAD && atomic_load(&client->transmit_vplay)) {
|
||||
const u32 rts = ntohl(header->timestamp); // RTS == RTP TimeStamp
|
||||
bool retry;
|
||||
do {
|
||||
retry = false;
|
||||
switch (us_rtpc_unwrap(client->rtpc, payload, payload_size, seq, rts)) {
|
||||
case US_RUR_SUCCESS:
|
||||
case US_RUR_BAD_PACKET:
|
||||
case US_RUR_INVALID_SEQ:
|
||||
case US_RUR_UNSUPPORTED_UNIT_TYPE:
|
||||
case US_RUR_DEPACKETIZATION_SKIPPED:
|
||||
break;
|
||||
case US_RUR_DEPACKETIZATION_FAILED_RETRY:
|
||||
retry = true;
|
||||
// fall through
|
||||
case US_RUR_DEPACKETIZATION_FAILED:
|
||||
client->gw->send_pli(client->session);
|
||||
break;
|
||||
}
|
||||
} while (retry);
|
||||
}
|
||||
} else {
|
||||
if (header->type == US_RTP_OPUS_PAYLOAD && atomic_load(&client->transmit_aplay)) {
|
||||
if (seq >= client->aplay_seq_next || (client->aplay_seq_next - seq) > 50) {
|
||||
// ^^^ In order or missing |OR| ^^^ In late sequence or sequence wrapped
|
||||
client->aplay_seq_next = seq + 1;
|
||||
us_ring_s *const ring = client->aplay_enc_ring;
|
||||
const int ri = us_ring_producer_acquire(ring, 0);
|
||||
if (ri < 0) {
|
||||
// US_LOG_ERROR("Session %p aplay ring is full", client->session);
|
||||
return;
|
||||
}
|
||||
us_au_encoded_s *enc = ring->items[ri];
|
||||
if ((uz)payload_size < US_ARRAY_LEN(enc->data)) {
|
||||
memcpy(enc->data, payload, payload_size);
|
||||
enc->used = payload_size;
|
||||
} else {
|
||||
enc->used = 0;
|
||||
}
|
||||
us_ring_producer_release(ring, ri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void us_janus_client_start_vplay(us_janus_client_s *client, us_ring_s* vplay_enc_ring) {
|
||||
if (atomic_exchange(&client->transmit_vplay, true)) {
|
||||
return;
|
||||
}
|
||||
US_A(client->vplay_enc_ring == NULL);
|
||||
client->vplay_enc_ring = vplay_enc_ring;
|
||||
}
|
||||
|
||||
/*void us_janus_client_stop_vplay(us_janus_client_s *client) {
|
||||
atomic_store(&client->transmit_vplay, false);
|
||||
client->vplay_enc_ring = NULL;
|
||||
}*/
|
||||
|
||||
static void *_video_thread(void *v_client) {
|
||||
return _common_thread(v_client, true);
|
||||
US_THREAD_SETTLE("us_cx_vcap");
|
||||
return _video_or_acap_thread(v_client, true);
|
||||
}
|
||||
|
||||
static void *_audio_thread(void *v_client) {
|
||||
return _common_thread(v_client, false);
|
||||
static void *_acap_thread(void *v_client) {
|
||||
US_THREAD_SETTLE("us_cx_acap");
|
||||
return _video_or_acap_thread(v_client, false);
|
||||
}
|
||||
|
||||
static void *_common_thread(void *v_client, bool video) {
|
||||
us_janus_client_s *const client = (us_janus_client_s *)v_client;
|
||||
us_queue_s *const queue = (video ? client->video_queue : client->audio_queue);
|
||||
assert(queue != NULL); // Audio may be NULL
|
||||
static void *_video_or_acap_thread(void *v_client, bool video) {
|
||||
us_janus_client_s *const client = v_client;
|
||||
us_ring_s *const ring = (video ? client->video_ring : client->acap_ring);
|
||||
US_A(ring != NULL);
|
||||
|
||||
while (!atomic_load(&client->stop)) {
|
||||
us_rtp_s *rtp;
|
||||
if (!us_queue_get(queue, (void **)&rtp, 0.1)) {
|
||||
if (rtp == NULL) {
|
||||
break;
|
||||
}
|
||||
const int ri = us_ring_consumer_acquire(ring, 0.1);
|
||||
if (ri < 0) {
|
||||
continue;
|
||||
}
|
||||
us_rtp_s rtp;
|
||||
memcpy(&rtp, ring->items[ri], sizeof(us_rtp_s));
|
||||
us_ring_consumer_release(ring, ri);
|
||||
|
||||
if (
|
||||
atomic_load(&client->transmit)
|
||||
&& (video || atomic_load(&client->transmit_audio))
|
||||
) {
|
||||
janus_plugin_rtp packet = {0};
|
||||
packet.video = rtp->video;
|
||||
packet.buffer = (char *)rtp->datagram;
|
||||
packet.length = rtp->used;
|
||||
if (
|
||||
atomic_load(&client->transmit)
|
||||
&& (video || atomic_load(&client->transmit_acap))
|
||||
) {
|
||||
us_rtp_write_ssrc(&rtp, (rtp.video ? client->video_ssrc : client->audio_ssrc));
|
||||
|
||||
janus_plugin_rtp packet = {
|
||||
.video = rtp.video,
|
||||
.buffer = (char*)rtp.datagram,
|
||||
.length = rtp.used,
|
||||
# if JANUS_PLUGIN_API_VERSION >= 100
|
||||
// The uStreamer Janus plugin places video in stream index 0 and audio
|
||||
// (if available) in stream index 1.
|
||||
packet.mindex = (rtp->video ? 0 : 1);
|
||||
.mindex = (rtp.video ? 0 : 1),
|
||||
# endif
|
||||
};
|
||||
janus_plugin_rtp_extensions_reset(&packet.extensions);
|
||||
|
||||
janus_plugin_rtp_extensions_reset(&packet.extensions);
|
||||
/*if (rtp->zero_playout_delay) {
|
||||
if (rtp.first_of_frame) {
|
||||
if (rtp.zero_playout_delay) {
|
||||
// https://github.com/pikvm/pikvm/issues/784
|
||||
packet.extensions.min_delay = 0;
|
||||
packet.extensions.max_delay = 0;
|
||||
} else {
|
||||
// Эти дефолты используются в Chrome/Safari/Firefox.
|
||||
// Работает всё одинаково, потому что у них общая кодовая база WebRTC.
|
||||
packet.extensions.min_delay = 0;
|
||||
// 10s - Chromium/WebRTC default
|
||||
// 3s - Firefox default
|
||||
packet.extensions.max_delay = 300; // == 3s, i.e. 10ms granularity
|
||||
}*/
|
||||
|
||||
client->gw->relay_rtp(client->session, &packet);
|
||||
packet.extensions.max_delay = 1000; // == 10s, i.e. 10ms granularity
|
||||
}
|
||||
}
|
||||
us_rtp_destroy(rtp);
|
||||
|
||||
if (rtp.video && rtp.first_of_frame) {
|
||||
packet.extensions.abs_capture_ts = rtp.grab_ntp_ts;
|
||||
}
|
||||
|
||||
if (rtp.video && rtp.last_of_frame) {
|
||||
uint video_orient = atomic_load(&client->video_orient);
|
||||
if (video_orient != 0) {
|
||||
// The extension rotates the video clockwise, but want it counterclockwise.
|
||||
// It's more intuitive for people who have seen a protractor at least once in their life.
|
||||
if (video_orient == 90) {
|
||||
video_orient = 270;
|
||||
} else if (video_orient == 270) {
|
||||
video_orient = 90;
|
||||
}
|
||||
packet.extensions.video_rotation = video_orient;
|
||||
}
|
||||
}
|
||||
|
||||
client->gw->relay_rtp(client->session, &packet);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void *_aplay_thread(void *v_client) {
|
||||
US_THREAD_SETTLE("us_cx_aplay");
|
||||
|
||||
us_janus_client_s *const client = v_client;
|
||||
|
||||
int err;
|
||||
OpusDecoder *dec = opus_decoder_create(US_RTP_OPUS_HZ, US_RTP_OPUS_CH, &err);
|
||||
US_A(err == 0);
|
||||
|
||||
while (!atomic_load(&client->stop)) {
|
||||
const int in_ri = us_ring_consumer_acquire(client->aplay_enc_ring, 0.1);
|
||||
if (in_ri < 0) {
|
||||
continue;
|
||||
}
|
||||
us_au_encoded_s *in = client->aplay_enc_ring->items[in_ri];
|
||||
|
||||
if (in->used == 0) {
|
||||
us_ring_consumer_release(client->aplay_enc_ring, in_ri);
|
||||
continue;
|
||||
}
|
||||
|
||||
const int out_ri = us_ring_producer_acquire(client->aplay_pcm_ring, 0);
|
||||
if (out_ri < 0) {
|
||||
US_LOG_ERROR("OPUS decoder queue is full");
|
||||
us_ring_consumer_release(client->aplay_enc_ring, in_ri);
|
||||
continue;
|
||||
}
|
||||
us_au_pcm_s *out = client->aplay_pcm_ring->items[out_ri];
|
||||
|
||||
const int frames = opus_decode(dec, in->data, in->used, out->data, US_AU_HZ_TO_FRAMES(US_RTP_OPUS_HZ), 0);
|
||||
us_ring_consumer_release(client->aplay_enc_ring, in_ri);
|
||||
|
||||
if (frames > 0) {
|
||||
out->frames = frames;
|
||||
} else {
|
||||
out->frames = 0;
|
||||
US_LOG_PERROR_OPUS(frames, "Fatal: Can't decode OPUS to PCM frame");
|
||||
}
|
||||
us_ring_producer_release(client->aplay_pcm_ring, out_ri);
|
||||
}
|
||||
|
||||
opus_decoder_destroy(dec);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,37 +22,49 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdatomic.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <pthread.h>
|
||||
#include <janus/plugins/plugin.h>
|
||||
|
||||
#include "uslibs/tools.h"
|
||||
#include "uslibs/threading.h"
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/list.h"
|
||||
#include "uslibs/ring.h"
|
||||
|
||||
#include "logging.h"
|
||||
#include "queue.h"
|
||||
#include "rtp.h"
|
||||
#include "rtpv.h"
|
||||
#include "rtpc.h"
|
||||
|
||||
|
||||
typedef struct us_janus_client_sx {
|
||||
typedef struct {
|
||||
janus_callbacks *gw;
|
||||
janus_plugin_session *session;
|
||||
atomic_bool transmit;
|
||||
atomic_bool transmit_audio;
|
||||
|
||||
pthread_t video_tid;
|
||||
pthread_t audio_tid;
|
||||
atomic_bool stop;
|
||||
u32 video_ssrc;
|
||||
u32 audio_ssrc;
|
||||
|
||||
us_queue_s *video_queue;
|
||||
us_queue_s *audio_queue;
|
||||
atomic_bool transmit;
|
||||
atomic_bool transmit_acap;
|
||||
atomic_bool transmit_vplay;
|
||||
atomic_bool transmit_aplay;
|
||||
atomic_uint video_orient;
|
||||
|
||||
US_LIST_STRUCT(struct us_janus_client_sx);
|
||||
pthread_t video_tid;
|
||||
pthread_t acap_tid;
|
||||
pthread_t aplay_tid;
|
||||
atomic_bool stop;
|
||||
|
||||
us_ring_s *video_ring;
|
||||
us_ring_s *acap_ring;
|
||||
|
||||
us_ring_s *aplay_enc_ring;
|
||||
u16 aplay_seq_next;
|
||||
us_ring_s *aplay_pcm_ring;
|
||||
|
||||
us_rtpc_s *rtpc;
|
||||
us_ring_s *vplay_enc_ring;
|
||||
|
||||
US_LIST_DECLARE;
|
||||
} us_janus_client_s;
|
||||
|
||||
|
||||
@@ -60,3 +72,7 @@ us_janus_client_s *us_janus_client_init(janus_callbacks *gw, janus_plugin_sessio
|
||||
void us_janus_client_destroy(us_janus_client_s *client);
|
||||
|
||||
void us_janus_client_send(us_janus_client_s *client, const us_rtp_s *rtp);
|
||||
void us_janus_client_recv(us_janus_client_s *client, janus_plugin_rtp *packet);
|
||||
|
||||
void us_janus_client_start_vplay(us_janus_client_s *client, us_ring_s* vplay_enc_ring);
|
||||
// void us_janus_client_stop_vplay(us_janus_client_s *client);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,8 +22,21 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <janus/config.h>
|
||||
#include <janus/plugins/plugin.h>
|
||||
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/tools.h"
|
||||
#include "uslibs/logging.h"
|
||||
|
||||
#include "const.h"
|
||||
|
||||
|
||||
static char *_get_value(janus_config *jcfg, const char *section, const char *option);
|
||||
static uint _get_uint(janus_config *jcfg, const char *section, const char *option, uint def, uint base);
|
||||
// static bool _get_bool(janus_config *jcfg, const char *section, const char *option, bool def);
|
||||
|
||||
|
||||
@@ -35,55 +48,81 @@ us_config_s *us_config_init(const char *config_dir_path) {
|
||||
janus_config *jcfg = NULL;
|
||||
|
||||
US_ASPRINTF(config_file_path, "%s/%s.jcfg", config_dir_path, US_PLUGIN_PACKAGE);
|
||||
US_JLOG_INFO("config", "Reading config file '%s' ...", config_file_path);
|
||||
US_LOG_INFO("Reading config file '%s' ...", config_file_path);
|
||||
|
||||
jcfg = janus_config_parse(config_file_path);
|
||||
if (jcfg == NULL) {
|
||||
US_JLOG_ERROR("config", "Can't read config");
|
||||
US_LOG_ERROR("Can't read config");
|
||||
goto error;
|
||||
}
|
||||
janus_config_print(jcfg);
|
||||
|
||||
if (
|
||||
(config->video_sink_name = _get_value(jcfg, "memsink", "object")) == NULL
|
||||
&& (config->video_sink_name = _get_value(jcfg, "video", "sink")) == NULL
|
||||
) {
|
||||
US_JLOG_ERROR("config", "Missing config value: video.sink (ex. memsink.object)");
|
||||
config->video_sink_name = _get_value(jcfg, "video", "sink");
|
||||
if (!us_str_is_ok(config->video_sink_name)) {
|
||||
US_LOG_ERROR("Missing config value: video.sink");
|
||||
goto error;
|
||||
}
|
||||
if ((config->audio_dev_name = _get_value(jcfg, "audio", "device")) != NULL) {
|
||||
if ((config->tc358743_dev_path = _get_value(jcfg, "audio", "tc358743")) == NULL) {
|
||||
US_JLOG_INFO("config", "Missing config value: audio.tc358743");
|
||||
|
||||
config->acap_dev_name = _get_value(jcfg, "acap", "device");
|
||||
if (us_str_is_ok(config->acap_dev_name)) {
|
||||
config->acap_hz = _get_uint(jcfg, "acap", "sampling_rate", 0, 10);
|
||||
config->tc358743_dev_path = _get_value(jcfg, "acap", "tc358743");
|
||||
if (config->acap_hz == 0 && !us_str_is_ok(config->tc358743_dev_path)) {
|
||||
US_LOG_ERROR("Either acap.sampling_rate or acap.tc358743 required");
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
config->aplay_dev_name = _get_value(jcfg, "aplay", "device");
|
||||
|
||||
config->vplay_sink_name = _get_value(jcfg, "vplay", "sink");
|
||||
config->vplay_sink_mode = _get_uint(jcfg, "vplay", "sink_mode", 0660, 8);
|
||||
config->vplay_dev_path = _get_value(jcfg, "vplay", "device");
|
||||
|
||||
goto ok;
|
||||
error:
|
||||
us_config_destroy(config);
|
||||
config = NULL;
|
||||
ok:
|
||||
US_DELETE(jcfg, janus_config_destroy);
|
||||
free(config_file_path);
|
||||
return config;
|
||||
|
||||
error:
|
||||
US_DELETE(config, us_config_destroy);
|
||||
|
||||
ok:
|
||||
US_DELETE(jcfg, janus_config_destroy);
|
||||
free(config_file_path);
|
||||
return config;
|
||||
}
|
||||
|
||||
void us_config_destroy(us_config_s *config) {
|
||||
US_DELETE(config->video_sink_name, free);
|
||||
US_DELETE(config->audio_dev_name, free);
|
||||
US_DELETE(config->acap_dev_name, free);
|
||||
US_DELETE(config->tc358743_dev_path, free);
|
||||
US_DELETE(config->aplay_dev_name, free);
|
||||
US_DELETE(config->vplay_sink_name, free);
|
||||
US_DELETE(config->vplay_dev_path, free);
|
||||
free(config);
|
||||
}
|
||||
|
||||
static char *_get_value(janus_config *jcfg, const char *section, const char *option) {
|
||||
janus_config_category *section_obj = janus_config_get_create(jcfg, NULL, janus_config_type_category, section);
|
||||
janus_config_item *option_obj = janus_config_get(jcfg, section_obj, janus_config_type_item, option);
|
||||
if (option_obj == NULL || option_obj->value == NULL || option_obj->value[0] == '\0') {
|
||||
if (option_obj == NULL || !us_str_is_ok(option_obj->value)) {
|
||||
return NULL;
|
||||
}
|
||||
return us_strdup(option_obj->value);
|
||||
}
|
||||
|
||||
static uint _get_uint(janus_config *jcfg, const char *section, const char *option, uint def, uint base) {
|
||||
char *const tmp = _get_value(jcfg, section, option);
|
||||
uint value = def;
|
||||
if (tmp != NULL) {
|
||||
errno = 0;
|
||||
value = (uint)strtoul(tmp, NULL, base);
|
||||
if (errno != 0) {
|
||||
value = def;
|
||||
}
|
||||
free(tmp);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/*static bool _get_bool(janus_config *jcfg, const char *section, const char *option, bool def) {
|
||||
char *const tmp = _get_value(jcfg, section, option);
|
||||
bool value = def;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,23 +22,22 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <janus/config.h>
|
||||
#include <janus/plugins/plugin.h>
|
||||
|
||||
#include "uslibs/tools.h"
|
||||
|
||||
#include "const.h"
|
||||
#include "logging.h"
|
||||
#include "uslibs/types.h"
|
||||
|
||||
|
||||
typedef struct {
|
||||
char *video_sink_name;
|
||||
|
||||
char *audio_dev_name;
|
||||
char *acap_dev_name;
|
||||
uint acap_hz;
|
||||
char *tc358743_dev_path;
|
||||
|
||||
char *aplay_dev_name;
|
||||
|
||||
char *vplay_sink_name;
|
||||
uint vplay_sink_mode;
|
||||
char *vplay_dev_path;
|
||||
} us_config_s;
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
|
||||
270
janus/src/h264.c
Normal file
270
janus/src/h264.c
Normal file
@@ -0,0 +1,270 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# This source file based on code of Janys-Gateway. #
|
||||
# #
|
||||
# Copyright (C) Lorenzo Miniero <lorenzo@meetecho.com> #
|
||||
# Copyright (C) 2026 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2026 Sergey Radionov <rsatom@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 "h264.h"
|
||||
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/tools.h"
|
||||
|
||||
|
||||
static INLINE u32 _eg_get_bit(const u8 *buf, u32 bit_offset) {
|
||||
return ((*(buf + (bit_offset >> 3))) >> (7 - (bit_offset & 0x07))) & 0x01;
|
||||
}
|
||||
|
||||
static INLINE bool _is_valid_eg_offset(u32 bit_offset, uz size) {
|
||||
return (bit_offset >> 3) + (bit_offset % 8 ? 1 : 0) < size;
|
||||
}
|
||||
|
||||
static bool _eg_skip_u1(const u8 *buf, u32 *bit_offset, uz size) {
|
||||
(void)buf;
|
||||
if (!_is_valid_eg_offset(*bit_offset, size)) {
|
||||
return false;
|
||||
}
|
||||
++(*bit_offset);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _eg_decode_u1(const u8 *buf, u32 *bit_offset, uz size, bool *out) {
|
||||
if (!_is_valid_eg_offset(*bit_offset, size)) {
|
||||
return false;
|
||||
}
|
||||
*out = (_eg_get_bit(buf, *bit_offset) != 0);
|
||||
++(*bit_offset);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _eg_skip_ev(const u8 *buf, u32 *bit_offset, uz size) {
|
||||
u32 zeros = 0;
|
||||
while (true) {
|
||||
if (!_is_valid_eg_offset(*bit_offset, size)) {
|
||||
return false;
|
||||
}
|
||||
if (_eg_get_bit(buf, (*bit_offset)++) != 0) {
|
||||
break;
|
||||
}
|
||||
++zeros;
|
||||
}
|
||||
for (; zeros; --zeros) {
|
||||
if (!_is_valid_eg_offset((*bit_offset)++, size)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _eg_decode_uev(const u8 *buf, u32 *bit_offset, uint size, u32 *out) {
|
||||
u32 zeros = 0;
|
||||
while (true) {
|
||||
if (!_is_valid_eg_offset(*bit_offset, size)) {
|
||||
return false;
|
||||
}
|
||||
if (_eg_get_bit(buf, (*bit_offset)++) != 0) {
|
||||
break;
|
||||
}
|
||||
++zeros;
|
||||
}
|
||||
u32 res = (1 << zeros);
|
||||
for (; zeros; --zeros) {
|
||||
if (!_is_valid_eg_offset(*bit_offset, size)) {
|
||||
return false;
|
||||
}
|
||||
res |= (_eg_get_bit(buf, *bit_offset) << (zeros - 1));
|
||||
++(*bit_offset);
|
||||
}
|
||||
*out = res - 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
int us_h264_parse_sps_nalu(const u8 *buf, uz size, uint *width, uint *height) {
|
||||
// NAL header + profile_idc + constraint_setX_flag, reserved_zero_2bits + level_idc
|
||||
if (size < 4) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// NAL header
|
||||
++buf;
|
||||
--size;
|
||||
|
||||
// profile_idc u(8) - Baseline profile
|
||||
const u8 profile_idc = *buf;
|
||||
if (profile_idc != 66) {
|
||||
return -1;
|
||||
}
|
||||
++buf;
|
||||
--size;
|
||||
|
||||
// constraint_set0_flag u(1)
|
||||
// constraint_set1_flag u(1)
|
||||
// constraint_set2_flag u(1)
|
||||
// constraint_set3_flag u(1)
|
||||
// constraint_set4_flag u(1)
|
||||
// constraint_set5_flag u(1)
|
||||
// reserved_zero_2bits u(2)
|
||||
++buf;
|
||||
--size;
|
||||
|
||||
// level_idc u(8)
|
||||
++buf;
|
||||
--size;
|
||||
|
||||
u32 bit_offset = 0;
|
||||
|
||||
// seq_parameter_set_id ue(v)
|
||||
if (!_eg_skip_ev(buf, &bit_offset, size)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// log2_max_frame_num_minus4 ue(v)
|
||||
u32 log2_max_frame_num_minus4;
|
||||
if (!_eg_decode_uev(buf, &bit_offset, size, &log2_max_frame_num_minus4)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// pic_order_cnt_type ue(v)
|
||||
u32 pic_order_cnt_type;
|
||||
if (!_eg_decode_uev(buf, &bit_offset, size, &pic_order_cnt_type)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (pic_order_cnt_type == 0) {
|
||||
// log2_max_pic_order_cnt_lsb_minus4 ue(v)
|
||||
if (!_eg_skip_ev(buf, &bit_offset, size)) {
|
||||
return -1;
|
||||
}
|
||||
} else if (pic_order_cnt_type == 1) {
|
||||
// delta_pic_order_always_zero_flag u(1)
|
||||
if (!_eg_skip_u1(buf, &bit_offset, size)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// offset_for_non_ref_pic se(v)
|
||||
if (!_eg_skip_ev(buf, &bit_offset, size)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// offset_for_top_to_bottom_field se(v)
|
||||
if (!_eg_skip_ev(buf, &bit_offset, size)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// num_ref_frames_in_pic_order_cnt_cycle ue(v)
|
||||
u32 num_ref_frames_in_pic_order_cnt_cycle;
|
||||
if (!_eg_decode_uev(buf, &bit_offset, size, &num_ref_frames_in_pic_order_cnt_cycle)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (u32 i = 0; i < num_ref_frames_in_pic_order_cnt_cycle; ++i) {
|
||||
// offset_for_ref_frame[i] se(v)
|
||||
if (!_eg_skip_ev(buf, &bit_offset, size)) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// max_num_ref_frames ue(v)
|
||||
if (!_eg_skip_ev(buf, &bit_offset, size)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// gaps_in_frame_num_value_allowed_flag u(1)
|
||||
if (!_eg_skip_u1(buf, &bit_offset, size)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// pic_width_in_mbs_minus1 ue(v)
|
||||
u32 pic_width_in_mbs_minus1;
|
||||
if (!_eg_decode_uev(buf, &bit_offset, size, &pic_width_in_mbs_minus1)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
//pic_height_in_map_units_minus1 ue(v)
|
||||
u32 pic_height_in_map_units_minus1;
|
||||
if (!_eg_decode_uev(buf, &bit_offset, size, &pic_height_in_map_units_minus1)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// frame_mbs_only_flag u(1)
|
||||
bool frame_mbs_only_flag;
|
||||
if (!_eg_decode_u1(buf, &bit_offset, size, &frame_mbs_only_flag)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!frame_mbs_only_flag) {
|
||||
// mb_adaptive_frame_field_flag u(1)
|
||||
if (!_eg_skip_u1(buf, &bit_offset, size)) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
//direct_8x8_inference_flag u(1)
|
||||
if (!_eg_skip_u1(buf, &bit_offset, size)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// frame_cropping_flag u(1)
|
||||
bool frame_cropping_flag;
|
||||
if (!_eg_decode_u1(buf, &bit_offset, size, &frame_cropping_flag)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
u32 frame_crop_left_offset = 0;
|
||||
u32 frame_crop_right_offset = 0;
|
||||
u32 frame_crop_top_offset = 0;
|
||||
u32 frame_crop_bottom_offset = 0;
|
||||
if (frame_cropping_flag) {
|
||||
// frame_crop_left_offset ue(v)
|
||||
if (!_eg_decode_uev(buf, &bit_offset, size, &frame_crop_left_offset)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// frame_crop_right_offset ue(v)
|
||||
if (!_eg_decode_uev(buf, &bit_offset, size, &frame_crop_right_offset)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// frame_crop_top_offset ue(v)
|
||||
if (!_eg_decode_uev(buf, &bit_offset, size, &frame_crop_top_offset)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// frame_crop_bottom_offset ue(v)
|
||||
if (!_eg_decode_uev(buf, &bit_offset, size, &frame_crop_bottom_offset)) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
*width = (
|
||||
(pic_width_in_mbs_minus1 + 1) * 16
|
||||
- frame_crop_left_offset * 2
|
||||
- frame_crop_right_offset * 2
|
||||
);
|
||||
*height = (
|
||||
(pic_height_in_map_units_minus1 + 1) * (2 - frame_mbs_only_flag) * 16
|
||||
- frame_crop_top_offset * 2
|
||||
- frame_crop_bottom_offset * 2
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2026 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2026 Sergey Radionov <rsatom@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 #
|
||||
@@ -22,12 +23,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <event2/util.h>
|
||||
#include <event2/http.h>
|
||||
#include <event2/keyvalq_struct.h>
|
||||
#include "uslibs/types.h"
|
||||
|
||||
|
||||
bool us_uri_get_true(struct evkeyvalq *params, const char *key);
|
||||
char *us_uri_get_string(struct evkeyvalq *params, const char *key);
|
||||
int us_h264_parse_sps_nalu(const u8 *buf, uz size, uint *width, uint *height);
|
||||
@@ -1,46 +0,0 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 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 <janus/plugins/plugin.h>
|
||||
|
||||
#include "uslibs/tools.h"
|
||||
|
||||
#include "const.h"
|
||||
|
||||
|
||||
#define US_JLOG_INFO(x_prefix, x_msg, ...) JANUS_LOG(LOG_INFO, "== %s/%-9s -- " x_msg "\n", US_PLUGIN_NAME, x_prefix, ##__VA_ARGS__)
|
||||
#define US_JLOG_WARN(x_prefix, x_msg, ...) JANUS_LOG(LOG_WARN, "== %s/%-9s -- " x_msg "\n", US_PLUGIN_NAME, x_prefix, ##__VA_ARGS__)
|
||||
#define US_JLOG_ERROR(x_prefix, x_msg, ...) JANUS_LOG(LOG_ERR, "== %s/%-9s -- " x_msg "\n", US_PLUGIN_NAME, x_prefix, ##__VA_ARGS__)
|
||||
|
||||
#define US_JLOG_PERROR(x_prefix, x_msg, ...) { \
|
||||
char *const m_perror_str = us_errno_to_string(errno); \
|
||||
JANUS_LOG(LOG_ERR, "[%s/%-9s] " x_msg ": %s\n", US_PLUGIN_NAME, x_prefix, ##__VA_ARGS__, m_perror_str); \
|
||||
free(m_perror_str); \
|
||||
}
|
||||
|
||||
#define US_ONCE(...) { \
|
||||
const int m_reported = __LINE__; \
|
||||
if (m_reported != once) { \
|
||||
__VA_ARGS__; \
|
||||
once = m_reported; \
|
||||
} \
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 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 "memsinkfd.h"
|
||||
|
||||
|
||||
int us_memsink_fd_wait_frame(int fd, us_memsink_shared_s* mem, uint64_t last_id) {
|
||||
const long double deadline_ts = us_get_now_monotonic() + 1; // wait_timeout
|
||||
long double now;
|
||||
do {
|
||||
const int result = us_flock_timedwait_monotonic(fd, 1); // lock_timeout
|
||||
now = us_get_now_monotonic();
|
||||
if (result < 0 && errno != EWOULDBLOCK) {
|
||||
US_JLOG_PERROR("video", "Can't lock memsink");
|
||||
return -1;
|
||||
} else if (result == 0) {
|
||||
if (mem->magic == US_MEMSINK_MAGIC && mem->version == US_MEMSINK_VERSION && mem->id != last_id) {
|
||||
return 0;
|
||||
}
|
||||
if (flock(fd, LOCK_UN) < 0) {
|
||||
US_JLOG_PERROR("video", "Can't unlock memsink");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
usleep(1000); // lock_polling
|
||||
} while (now < deadline_ts);
|
||||
return -2;
|
||||
}
|
||||
|
||||
us_frame_s *us_memsink_fd_get_frame(int fd, us_memsink_shared_s *mem, uint64_t *frame_id, bool key_required) {
|
||||
us_frame_s *frame = us_frame_init();
|
||||
us_frame_set_data(frame, mem->data, mem->used);
|
||||
US_FRAME_COPY_META(mem, frame);
|
||||
*frame_id = mem->id;
|
||||
mem->last_client_ts = us_get_now_monotonic();
|
||||
if (key_required) {
|
||||
mem->key_requested = true;
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
if (frame->format != V4L2_PIX_FMT_H264) {
|
||||
US_JLOG_ERROR("video", "Got non-H264 frame from memsink");
|
||||
ok = false;
|
||||
}
|
||||
if (flock(fd, LOCK_UN) < 0) {
|
||||
US_JLOG_PERROR("video", "Can't unlock memsink");
|
||||
ok = false;
|
||||
}
|
||||
if (!ok) {
|
||||
us_frame_destroy(frame);
|
||||
frame = NULL;
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
# This source file is partially based on this code: #
|
||||
# - https://github.com/catid/kvm/blob/master/kvm_pipeline/src #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -25,39 +25,46 @@
|
||||
|
||||
#include "rtp.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
us_rtp_s *us_rtp_init(unsigned payload, bool video) {
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/tools.h"
|
||||
|
||||
|
||||
us_rtp_s *us_rtp_init(void) {
|
||||
us_rtp_s *rtp;
|
||||
US_CALLOC(rtp, 1);
|
||||
rtp->payload = payload;
|
||||
rtp->video = video;
|
||||
rtp->ssrc = us_triple_u32(us_get_now_monotonic_u64());
|
||||
return rtp;
|
||||
}
|
||||
|
||||
us_rtp_s *us_rtp_dup(const us_rtp_s *rtp) {
|
||||
us_rtp_s *new;
|
||||
US_CALLOC(new, 1);
|
||||
memcpy(new, rtp, sizeof(us_rtp_s));
|
||||
return new;
|
||||
}
|
||||
|
||||
void us_rtp_destroy(us_rtp_s *rtp) {
|
||||
free(rtp);
|
||||
}
|
||||
|
||||
void us_rtp_write_header(us_rtp_s *rtp, uint32_t pts, bool marked) {
|
||||
uint32_t word0 = 0x80000000;
|
||||
if (marked) {
|
||||
void us_rtp_assign(us_rtp_s *rtp, uint payload, bool video) {
|
||||
rtp->payload = payload;
|
||||
rtp->video = video;
|
||||
}
|
||||
|
||||
#define WRITE_BE_U32(x_offset, x_value) \
|
||||
*((u32*)(rtp->datagram + x_offset)) = __builtin_bswap32(x_value)
|
||||
|
||||
void us_rtp_write_header(us_rtp_s *rtp, u32 pts, bool last_header) {
|
||||
u32 word0 = 0x80000000;
|
||||
if (last_header) {
|
||||
word0 |= 1 << 23;
|
||||
}
|
||||
word0 |= (rtp->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
|
||||
WRITE_BE_U32(8, 0); // SSRC should be written separately for each client
|
||||
}
|
||||
|
||||
void us_rtp_write_ssrc(us_rtp_s *rtp, u32 ssrc) {
|
||||
WRITE_BE_U32(8, ssrc);
|
||||
}
|
||||
|
||||
#undef WRITE_BE_U32
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,36 +22,53 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/tools.h"
|
||||
|
||||
|
||||
// https://stackoverflow.com/questions/47635545/why-webrtc-chose-rtp-max-packet-size-to-1200-bytes
|
||||
#define US_RTP_DATAGRAM_SIZE 1200
|
||||
// Max RTP size for WebRTC is 1200 bytes:
|
||||
// - https://stackoverflow.com/questions/47635545/why-webrtc-chose-rtp-max-packet-size-to-1200-bytes
|
||||
// But(!) Tailscale has 1280 MTU. So to fit it required to subtract:
|
||||
// 1. possible RTP extensions (see sdp.c)
|
||||
// 2. additional SRTP fields (>= 10 bytes)
|
||||
// 3. UDP header (8 bytes)
|
||||
// 4. IPv6 header (40 bytes)
|
||||
// Finally it looks like below value should be good enought for all cases
|
||||
#define US_RTP_TOTAL_SIZE 1150
|
||||
#define US_RTP_HEADER_SIZE 12
|
||||
#define US_RTP_PAYLOAD_SIZE (US_RTP_TOTAL_SIZE - US_RTP_HEADER_SIZE)
|
||||
|
||||
#define US_RTP_H264_PAYLOAD 96
|
||||
#define US_RTP_OPUS_PAYLOAD 111
|
||||
|
||||
#define US_RTP_OPUS_HZ 48000
|
||||
#define US_RTP_OPUS_CH 2
|
||||
|
||||
|
||||
typedef struct {
|
||||
unsigned payload;
|
||||
bool video;
|
||||
uint32_t ssrc;
|
||||
uint payload;
|
||||
bool video;
|
||||
|
||||
uint16_t seq;
|
||||
uint8_t datagram[US_RTP_DATAGRAM_SIZE];
|
||||
size_t used;
|
||||
bool zero_playout_delay;
|
||||
u16 seq;
|
||||
u8 datagram[US_RTP_TOTAL_SIZE];
|
||||
uz used;
|
||||
|
||||
bool first_of_frame;
|
||||
bool last_of_frame;
|
||||
bool zero_playout_delay;
|
||||
u64 grab_ntp_ts;
|
||||
} us_rtp_s;
|
||||
|
||||
typedef void (*us_rtp_callback_f)(const us_rtp_s *rtp);
|
||||
|
||||
|
||||
us_rtp_s *us_rtp_init(unsigned payload, bool video);
|
||||
us_rtp_s *us_rtp_dup(const us_rtp_s *rtp);
|
||||
static inline u32 us_get_ssrc(void) {
|
||||
return us_triple_u32(us_get_now_monotonic_u64());
|
||||
}
|
||||
|
||||
us_rtp_s *us_rtp_init(void);
|
||||
void us_rtp_destroy(us_rtp_s *rtp);
|
||||
|
||||
void us_rtp_write_header(us_rtp_s *rtp, uint32_t pts, bool marked);
|
||||
void us_rtp_assign(us_rtp_s *rtp, uint payload, bool video);
|
||||
void us_rtp_write_header(us_rtp_s *rtp, u32 pts, bool last_header);
|
||||
void us_rtp_write_ssrc(us_rtp_s *rtp, u32 ssrc);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,11 +22,19 @@
|
||||
|
||||
#include "rtpa.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/tools.h"
|
||||
|
||||
#include "rtp.h"
|
||||
|
||||
|
||||
us_rtpa_s *us_rtpa_init(us_rtp_callback_f callback) {
|
||||
us_rtpa_s *rtpa;
|
||||
US_CALLOC(rtpa, 1);
|
||||
rtpa->rtp = us_rtp_init(111, false);
|
||||
rtpa->rtp = us_rtp_init();
|
||||
us_rtp_assign(rtpa->rtp, US_RTP_OPUS_PAYLOAD, false);
|
||||
rtpa->callback = callback;
|
||||
return rtpa;
|
||||
}
|
||||
@@ -36,28 +44,8 @@ void us_rtpa_destroy(us_rtpa_s *rtpa) {
|
||||
free(rtpa);
|
||||
}
|
||||
|
||||
char *us_rtpa_make_sdp(us_rtpa_s *rtpa) {
|
||||
# define PAYLOAD rtpa->rtp->payload
|
||||
char *sdp;
|
||||
US_ASPRINTF(sdp,
|
||||
"m=audio 1 RTP/SAVPF %u" RN
|
||||
"c=IN IP4 0.0.0.0" RN
|
||||
"a=rtpmap:%u OPUS/48000/2" RN
|
||||
// "a=fmtp:%u useinbandfec=1" RN
|
||||
"a=rtcp-fb:%u nack" RN
|
||||
"a=rtcp-fb:%u nack pli" RN
|
||||
"a=rtcp-fb:%u goog-remb" RN
|
||||
"a=ssrc:%" PRIu32 " cname:ustreamer" RN
|
||||
"a=sendonly" RN,
|
||||
PAYLOAD, PAYLOAD, PAYLOAD, PAYLOAD, PAYLOAD, // PAYLOAD,
|
||||
rtpa->rtp->ssrc
|
||||
);
|
||||
# undef PAYLOAD
|
||||
return sdp;
|
||||
}
|
||||
|
||||
void us_rtpa_wrap(us_rtpa_s *rtpa, const uint8_t *data, size_t size, uint32_t pts) {
|
||||
if (size + US_RTP_HEADER_SIZE <= US_RTP_DATAGRAM_SIZE) {
|
||||
void us_rtpa_wrap(us_rtpa_s *rtpa, const u8 *data, uz size, u32 pts) {
|
||||
if (size + US_RTP_HEADER_SIZE <= US_RTP_TOTAL_SIZE) {
|
||||
us_rtp_write_header(rtpa->rtp, pts, false);
|
||||
memcpy(rtpa->rtp->datagram + US_RTP_HEADER_SIZE, data, size);
|
||||
rtpa->rtp->used = size + US_RTP_HEADER_SIZE;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,15 +22,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "uslibs/tools.h"
|
||||
#include "uslibs/threading.h"
|
||||
#include "uslibs/types.h"
|
||||
|
||||
#include "rtp.h"
|
||||
|
||||
@@ -44,5 +36,4 @@ typedef struct {
|
||||
us_rtpa_s *us_rtpa_init(us_rtp_callback_f callback);
|
||||
void us_rtpa_destroy(us_rtpa_s *rtpa);
|
||||
|
||||
char *us_rtpa_make_sdp(us_rtpa_s *rtpa);
|
||||
void us_rtpa_wrap(us_rtpa_s *rtpa, const uint8_t *data, size_t size, uint32_t pts);
|
||||
void us_rtpa_wrap(us_rtpa_s *rtpa, const u8 *data, uz size, u32 pts);
|
||||
|
||||
316
janus/src/rtpc.c
Normal file
316
janus/src/rtpc.c
Normal file
@@ -0,0 +1,316 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2026 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2026 Sergey Radionov <rsatom@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 "rtpc.h"
|
||||
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#include "uslibs/logging.h"
|
||||
|
||||
#include "h264.h"
|
||||
|
||||
|
||||
// H.264 fragment types. Commented types are not used in WebRTC.
|
||||
enum {
|
||||
_FR_REGULAR_NAL_MIN = 1,
|
||||
_FR_REGULAR_NAL_MAX = 23,
|
||||
_FR_STAP_A = 24, // Single Time Aggregation Packet, Type A (STAP-A)
|
||||
// _FR_STAP_B = 25, // Single Time Aggregation Packet, Type B (STAP-B)
|
||||
// _FR_MTAP_16 = 26, // Multi-Time Aggregation Packet, Type A (MTAP-16)
|
||||
// _FR_MTAP_24 = 27, // Multi-Time Aggregation Packet, Type B (MTAP-24)
|
||||
_FR_FU_A = 28, // Fragmentation unit (FU-A)
|
||||
// _FR_FU_B = 29, // Fragmentation unit (FU-B)
|
||||
};
|
||||
|
||||
enum {
|
||||
_NALU_SPS = 7
|
||||
};
|
||||
|
||||
|
||||
static int _update_sequence_for_new_packet(us_rtpc_s *rtpc, u16 seq);
|
||||
static ldf _rtp_to_local_ts(us_rtpc_s *rtpc, u32 rts);
|
||||
static void _update_timestamps_for_new_packet(us_rtpc_s *rtpc, u32 rts);
|
||||
|
||||
static void _unwrapping_begin(us_rtpc_s *rtpc);
|
||||
static void _unwrapping_end(us_rtpc_s *rtpc);
|
||||
static void _unwrap_whole(us_rtpc_s *rtpc, const u8 *data, u16 size);
|
||||
|
||||
|
||||
us_rtpc_s *us_rtpc_init(us_frame_callback_f callback, void *user_data) {
|
||||
us_rtpc_s *rtpc;
|
||||
US_CALLOC(rtpc, 1);
|
||||
rtpc->user_data = user_data;
|
||||
rtpc->frame = us_frame_init();
|
||||
rtpc->callback = callback;
|
||||
return rtpc;
|
||||
}
|
||||
|
||||
void us_rtpc_destroy(us_rtpc_s *rtpc) {
|
||||
us_frame_destroy(rtpc->frame);
|
||||
free(rtpc);
|
||||
}
|
||||
|
||||
us_rtpc_unwrap_result_e us_rtpc_unwrap(
|
||||
us_rtpc_s *rtpc,
|
||||
const u8 *payload,
|
||||
int payload_size,
|
||||
u16 seq,
|
||||
u32 rts
|
||||
) {
|
||||
US_A(payload != NULL);
|
||||
US_A(payload_size >= 1);
|
||||
|
||||
if (_update_sequence_for_new_packet(rtpc, seq) < 0) {
|
||||
return US_RUR_INVALID_SEQ;
|
||||
}
|
||||
_update_timestamps_for_new_packet(rtpc, rts);
|
||||
|
||||
rtpc->had_packets = true;
|
||||
|
||||
const u8 fragment_type = payload[0] & 0x1F;
|
||||
|
||||
if (rtpc->fu && (rtpc->fu_rts != rts || fragment_type != _FR_FU_A)) {
|
||||
rtpc->fu = false;
|
||||
if (!rtpc->fu_is_bad) { // Not reported yet
|
||||
return US_RUR_DEPACKETIZATION_FAILED_RETRY;
|
||||
}
|
||||
}
|
||||
|
||||
if (fragment_type >= _FR_REGULAR_NAL_MIN && fragment_type <= _FR_REGULAR_NAL_MAX) {
|
||||
_unwrap_whole(rtpc, payload, payload_size);
|
||||
|
||||
} else if (fragment_type == _FR_STAP_A) { // Single Time Aggregation Packet, Type A (STAP-A).
|
||||
const u8 *aggregated_unit = payload + 1; // skip aggregation header (1 byte)
|
||||
payload_size -= 1;
|
||||
|
||||
while (payload_size > 0) {
|
||||
if (payload_size < 2) { // Aggregated NAL unit size (2 bytes) is missing
|
||||
US_LOG_ERROR("Aggregated NALU size is missing: seq=%u, rts=%u", seq, rts);
|
||||
return US_RUR_BAD_PACKET;
|
||||
}
|
||||
|
||||
const u16 nalu_size = ntohs(*((const u16 *)aggregated_unit));
|
||||
aggregated_unit += 2;
|
||||
payload_size -= 2;
|
||||
|
||||
if (nalu_size == 0) {
|
||||
US_LOG_ERROR("Aggregated NALU size is zero: seq=%u, rts=%u", seq, rts);
|
||||
return US_RUR_BAD_PACKET;
|
||||
}
|
||||
|
||||
if (payload_size < nalu_size) { // aggregated unit size declared as bigger than available data size
|
||||
US_LOG_ERROR("Aggregated NALU size is too big: seq=%u, rts=%u", seq, rts);
|
||||
return US_RUR_BAD_PACKET;
|
||||
}
|
||||
|
||||
_unwrap_whole(rtpc, aggregated_unit, nalu_size);
|
||||
|
||||
aggregated_unit += nalu_size;
|
||||
payload_size -= nalu_size;
|
||||
}
|
||||
|
||||
} else if (fragment_type == _FR_FU_A) { // Fragmentation unit (FU-A).
|
||||
if (payload_size < 2) { // FU header is missing (2 bytes)
|
||||
US_LOG_ERROR("FU header is missing: seq=%u, rts=%u", seq, rts);
|
||||
return US_RUR_BAD_PACKET;
|
||||
}
|
||||
|
||||
const u8 fu_indicator = payload[0];
|
||||
const u8 fu_header = payload[1];
|
||||
const bool first_fragment = !!(fu_header & 0x80);
|
||||
const bool last_fragment = !!(fu_header & 0x40);
|
||||
|
||||
if (first_fragment && last_fragment) { // should never happen according to RFC
|
||||
return US_RUR_BAD_PACKET;
|
||||
}
|
||||
|
||||
if (rtpc->fu && rtpc->fu_is_bad) {
|
||||
return US_RUR_DEPACKETIZATION_SKIPPED;
|
||||
}
|
||||
|
||||
if (!rtpc->fu) {
|
||||
rtpc->fu = true;
|
||||
rtpc->fu_seq = seq;
|
||||
rtpc->fu_rts = rts;
|
||||
|
||||
if (!first_fragment) {
|
||||
// US_LOG_INFO("Ignoring FU without a first fragment: seq=%u, rts=%u", seq, rts);
|
||||
rtpc->fu_is_bad = true;
|
||||
return US_RUR_DEPACKETIZATION_FAILED;
|
||||
}
|
||||
rtpc->fu_is_bad = false;
|
||||
|
||||
const u8 header = (fu_indicator & 0xE0) | (fu_header & 0x1F);
|
||||
_unwrapping_begin(rtpc);
|
||||
us_frame_append_data(rtpc->frame, &header, sizeof(header));
|
||||
// - FU indicator - FU header
|
||||
us_frame_append_data(rtpc->frame, (const u8 *)payload + 2, payload_size - 2);
|
||||
|
||||
} else if (!first_fragment && (seq == rtpc->fu_seq + 1) && (rts == rtpc->fu_rts)) {
|
||||
// ^^^ Already had first fragment
|
||||
rtpc->fu_seq = seq;
|
||||
|
||||
// - FU indicator - FU header
|
||||
us_frame_append_data(rtpc->frame, (const u8 *)payload + 2, payload_size - 2);
|
||||
|
||||
if (last_fragment) {
|
||||
_unwrapping_end(rtpc);
|
||||
rtpc->fu = false;
|
||||
}
|
||||
|
||||
} else {
|
||||
// US_LOG_ERROR("Something went wrong with FU processing: seq=%u, rts=%u", seq, rts);
|
||||
rtpc->fu_is_bad = true;
|
||||
return US_RUR_DEPACKETIZATION_FAILED;
|
||||
}
|
||||
|
||||
} else {
|
||||
US_LOG_ERROR("Unsupported video unit type=%x: seq=%u, rts=%u", fragment_type, seq, rts);
|
||||
return US_RUR_UNSUPPORTED_UNIT_TYPE;
|
||||
}
|
||||
return US_RUR_SUCCESS;
|
||||
}
|
||||
|
||||
void us_rtpc_sync_timestamp(us_rtpc_s *rtpc, ldf ntp_ts, u32 packet_rts) {
|
||||
if (!rtpc->had_packets) {
|
||||
return;
|
||||
}
|
||||
if (rtpc->reference_ntp_ts > ntp_ts) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rtpc->reference_ntp_ts == 0.L) {
|
||||
if (rtpc->reference_packet_rts < packet_rts) {
|
||||
const ldf diff = (packet_rts - rtpc->reference_packet_rts) / 90000.L;
|
||||
rtpc->reference_local_ts += diff;
|
||||
rtpc->reference_packet_rts = packet_rts;
|
||||
rtpc->reference_ntp_ts = ntp_ts;
|
||||
}
|
||||
} else {
|
||||
const ldf diff = ntp_ts - rtpc->reference_ntp_ts;
|
||||
rtpc->reference_local_ts += diff;
|
||||
rtpc->reference_packet_rts = packet_rts;
|
||||
rtpc->reference_ntp_ts = ntp_ts;
|
||||
}
|
||||
}
|
||||
|
||||
static int _update_sequence_for_new_packet(us_rtpc_s *rtpc, u16 seq) {
|
||||
if (
|
||||
rtpc->had_packets
|
||||
&& (
|
||||
rtpc->last_packet_seq == seq
|
||||
|| (rtpc->last_packet_seq < seq && seq - rtpc->last_packet_seq > (UINT16_MAX >> 1))
|
||||
|| (seq < rtpc->last_packet_seq && rtpc->last_packet_seq - seq < (UINT16_MAX >> 1))
|
||||
)
|
||||
) {
|
||||
return -1;
|
||||
}
|
||||
rtpc->last_packet_seq = seq;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static ldf _rtp_to_local_ts(us_rtpc_s *rtpc, u32 rts) {
|
||||
if (rtpc->reference_packet_rts <= rts) {
|
||||
return rtpc->reference_local_ts + (rts - rtpc->reference_packet_rts) / 90000.L;
|
||||
} else {
|
||||
return rtpc->reference_local_ts - (rtpc->reference_packet_rts - rts) / 90000.L;
|
||||
}
|
||||
}
|
||||
|
||||
static void _update_timestamps_for_new_packet(us_rtpc_s *rtpc, u32 rts) {
|
||||
const ldf now_ts = us_get_now_monotonic();
|
||||
ldf ts; // Fill for any goto
|
||||
|
||||
if (!rtpc->had_packets) {
|
||||
ts = _rtp_to_local_ts(rtpc, rts); // Used in packet_update
|
||||
goto full_update;
|
||||
}
|
||||
if (rtpc->last_packet_rts != rts) { // If this is a new frame
|
||||
ts = _rtp_to_local_ts(rtpc, rts);
|
||||
if (rtpc->last_packet_rts > rts || fabsl(now_ts - ts) > .1L) {
|
||||
// ^ RTP timestamp wrapped |OR| ^ Too significant mistake
|
||||
goto full_update;
|
||||
}
|
||||
goto packet_update;
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
full_update:
|
||||
rtpc->reference_local_ts = now_ts;
|
||||
rtpc->reference_packet_rts = rts;
|
||||
rtpc->reference_ntp_ts = 0.L; // Force resync
|
||||
// US_LOG_INFO("Reference timestamps reseted");
|
||||
packet_update:
|
||||
rtpc->last_packet_rts = rts;
|
||||
rtpc->frame_ts = ts;
|
||||
}
|
||||
|
||||
static void _unwrapping_begin(us_rtpc_s *rtpc) {
|
||||
us_frame_s *const frame = rtpc->frame;
|
||||
u8 *const data = frame->data;
|
||||
const uz allocated = frame->allocated;
|
||||
|
||||
// Reset the content, start from scratch
|
||||
memset(frame, 0, sizeof(us_frame_s));
|
||||
frame->data = data;
|
||||
frame->allocated = allocated;
|
||||
|
||||
static const u8 nalu_start_code[] = {0, 0, 0, 1};
|
||||
us_frame_set_data(rtpc->frame, nalu_start_code, sizeof(nalu_start_code));
|
||||
}
|
||||
|
||||
static void _unwrapping_end(us_rtpc_s *rtpc) {
|
||||
us_frame_s *const frame = rtpc->frame;
|
||||
frame->format = V4L2_PIX_FMT_H264;
|
||||
frame->width = rtpc->frame_width;
|
||||
frame->height = rtpc->frame_height;
|
||||
frame->grab_begin_ts = rtpc->frame_ts;
|
||||
frame->grab_end_ts = rtpc->frame_ts;
|
||||
|
||||
// Exchange frames
|
||||
rtpc->frame = rtpc->callback(frame, rtpc->user_data);
|
||||
US_A(rtpc->frame != NULL);
|
||||
}
|
||||
|
||||
static void _unwrap_whole(us_rtpc_s *rtpc, const u8 *data, u16 size) {
|
||||
US_A(size >= 1);
|
||||
|
||||
const u8 nalu_type = (data[0] & 0x1F);
|
||||
if (nalu_type == _NALU_SPS) {
|
||||
uint width = 0;
|
||||
uint height = 0;
|
||||
if (
|
||||
!us_h264_parse_sps_nalu(data, size, &width, &height)
|
||||
&& (rtpc->frame_width != width || rtpc->frame_height != height)
|
||||
) {
|
||||
US_LOG_INFO("Frame size updated: width=%u, height=%u", width, height);
|
||||
rtpc->frame_width = width;
|
||||
rtpc->frame_height = height;
|
||||
}
|
||||
}
|
||||
|
||||
_unwrapping_begin(rtpc);
|
||||
us_frame_append_data(rtpc->frame, data, size);
|
||||
_unwrapping_end(rtpc);
|
||||
}
|
||||
83
janus/src/rtpc.h
Normal file
83
janus/src/rtpc.h
Normal file
@@ -0,0 +1,83 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2026 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2026 Sergey Radionov <rsatom@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 <janus/rtp.h>
|
||||
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/frame.h"
|
||||
|
||||
|
||||
// Should return:
|
||||
// * input frame
|
||||
// * replacement frame - to use it in following parsing
|
||||
typedef us_frame_s *(*us_frame_callback_f)(us_frame_s *frame, void *user_data);
|
||||
|
||||
typedef struct { // *_rts == [R]TP [T]ime[S]tamp
|
||||
us_frame_callback_f callback;
|
||||
void *user_data;
|
||||
|
||||
bool had_packets; // Были ли вообще какие-то пакеты когда-то, взводится один раз
|
||||
|
||||
ldf reference_local_ts; // Локальное время
|
||||
u32 reference_packet_rts;
|
||||
ldf reference_ntp_ts;
|
||||
|
||||
u16 last_packet_seq;
|
||||
u32 last_packet_rts; // Время из последнего полученного пакета
|
||||
|
||||
bool fu;
|
||||
u16 fu_seq;
|
||||
u32 fu_rts;
|
||||
bool fu_is_bad;
|
||||
|
||||
us_frame_s *frame;
|
||||
uint frame_width;
|
||||
uint frame_height;
|
||||
ldf frame_ts; // in case of fu, time of very first packet
|
||||
} us_rtpc_s;
|
||||
|
||||
typedef enum {
|
||||
US_RUR_SUCCESS = 0,
|
||||
US_RUR_BAD_PACKET = -1,
|
||||
US_RUR_INVALID_SEQ = -2,
|
||||
US_RUR_DEPACKETIZATION_FAILED = -3,
|
||||
// Incomplete FU found. Current packet not handled.
|
||||
// It's required to call us_rtpc_unwrap one else time with the same packet.
|
||||
US_RUR_DEPACKETIZATION_FAILED_RETRY = -4,
|
||||
US_RUR_DEPACKETIZATION_SKIPPED = -5,
|
||||
US_RUR_UNSUPPORTED_UNIT_TYPE = -6,
|
||||
} us_rtpc_unwrap_result_e;
|
||||
|
||||
us_rtpc_s *us_rtpc_init(us_frame_callback_f callback, void *user_data);
|
||||
void us_rtpc_destroy(us_rtpc_s *rtpc);
|
||||
|
||||
us_rtpc_unwrap_result_e us_rtpc_unwrap(
|
||||
us_rtpc_s *rtpc,
|
||||
const u8 *payload,
|
||||
int payload_size,
|
||||
u16 seq,
|
||||
u32 rts);
|
||||
|
||||
void us_rtpc_sync_timestamp(us_rtpc_s *rtpc, ldf ntp_ts, u32 packet_rts);
|
||||
250
janus/src/rtpv.c
250
janus/src/rtpv.c
@@ -5,7 +5,7 @@
|
||||
# This source file is partially based on this code: #
|
||||
# - https://github.com/catid/kvm/blob/master/kvm_pipeline/src #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -25,16 +25,30 @@
|
||||
|
||||
#include "rtpv.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
void _rtpv_process_nalu(us_rtpv_s *rtpv, const uint8_t *data, size_t size, uint32_t pts, bool marked);
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
static ssize_t _find_annexb(const uint8_t *data, size_t size);
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/tools.h"
|
||||
#include "uslibs/frame.h"
|
||||
|
||||
#include "rtp.h"
|
||||
|
||||
|
||||
static sz _find_annexb(const u8 *data, uz size);
|
||||
static bool _is_nalu_small(uz size);
|
||||
|
||||
static void _rtpv_process_nalu(us_rtpv_s *rtpv, const u8 *data, uz size, u32 pts, bool first_nalu, bool last_nalu);
|
||||
static void _rtpv_process_nalu_small(us_rtpv_s *rtpv, const u8 *data, uz size, u32 pts, bool first_nalu, bool last_nalu);
|
||||
static void _rtpv_process_nalu_big(us_rtpv_s *rtpv, const u8 *data, uz size, u32 pts, bool first_nalu, bool last_nalu);
|
||||
|
||||
|
||||
us_rtpv_s *us_rtpv_init(us_rtp_callback_f callback) {
|
||||
us_rtpv_s *rtpv;
|
||||
US_CALLOC(rtpv, 1);
|
||||
rtpv->rtp = us_rtp_init(96, true);
|
||||
rtpv->rtp = us_rtp_init();
|
||||
us_rtp_assign(rtpv->rtp, US_RTP_H264_PAYLOAD, true);
|
||||
rtpv->callback = callback;
|
||||
return rtpv;
|
||||
}
|
||||
@@ -44,129 +58,68 @@ void us_rtpv_destroy(us_rtpv_s *rtpv) {
|
||||
free(rtpv);
|
||||
}
|
||||
|
||||
char *us_rtpv_make_sdp(us_rtpv_s *rtpv) {
|
||||
# define PAYLOAD rtpv->rtp->payload
|
||||
// https://tools.ietf.org/html/rfc6184
|
||||
// https://github.com/meetecho/janus-gateway/issues/2443
|
||||
char *sdp;
|
||||
US_ASPRINTF(sdp,
|
||||
"m=video 1 RTP/SAVPF %u" RN
|
||||
"c=IN IP4 0.0.0.0" RN
|
||||
"a=rtpmap:%u H264/90000" RN
|
||||
"a=fmtp:%u profile-level-id=42E01F" RN
|
||||
"a=fmtp:%u packetization-mode=1" RN
|
||||
"a=rtcp-fb:%u nack" RN
|
||||
"a=rtcp-fb:%u nack pli" RN
|
||||
"a=rtcp-fb:%u goog-remb" RN
|
||||
"a=ssrc:%" PRIu32 " cname:ustreamer" RN
|
||||
"a=extmap:1 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay" RN
|
||||
"a=sendonly" RN,
|
||||
PAYLOAD, PAYLOAD, PAYLOAD, PAYLOAD,
|
||||
PAYLOAD, PAYLOAD, PAYLOAD,
|
||||
rtpv->rtp->ssrc
|
||||
);
|
||||
return sdp;
|
||||
# undef PAYLOAD
|
||||
}
|
||||
|
||||
#define _PRE 3 // Annex B prefix length
|
||||
|
||||
void us_rtpv_wrap(us_rtpv_s *rtpv, const us_frame_s *frame, bool zero_playout_delay) {
|
||||
// There is a complicated logic here but everything works as it should:
|
||||
// - https://github.com/pikvm/ustreamer/issues/115#issuecomment-893071775
|
||||
US_A(frame->format == V4L2_PIX_FMT_H264);
|
||||
|
||||
assert(frame->format == V4L2_PIX_FMT_H264);
|
||||
|
||||
rtpv->rtp->zero_playout_delay = zero_playout_delay;
|
||||
|
||||
const uint32_t pts = us_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 *const data = frame->data + last_offset + _PRE;
|
||||
size_t size = offset - last_offset - _PRE;
|
||||
if (data[size - 1] == 0) { // Check for extra 00
|
||||
--size;
|
||||
}
|
||||
_rtpv_process_nalu(rtpv, data, size, pts, false);
|
||||
}
|
||||
|
||||
last_offset = offset;
|
||||
}
|
||||
|
||||
if (last_offset >= 0) {
|
||||
const uint8_t *const data = frame->data + last_offset + _PRE;
|
||||
size_t size = frame->used - last_offset - _PRE;
|
||||
_rtpv_process_nalu(rtpv, data, size, pts, true);
|
||||
}
|
||||
}
|
||||
|
||||
void _rtpv_process_nalu(us_rtpv_s *rtpv, const uint8_t *data, size_t size, uint32_t pts, bool marked) {
|
||||
# define DG rtpv->rtp->datagram
|
||||
|
||||
const unsigned ref_idc = (data[0] >> 5) & 3;
|
||||
const unsigned type = data[0] & 0x1F;
|
||||
|
||||
if (size + US_RTP_HEADER_SIZE <= US_RTP_DATAGRAM_SIZE) {
|
||||
us_rtp_write_header(rtpv->rtp, pts, marked);
|
||||
memcpy(DG + US_RTP_HEADER_SIZE, data, size);
|
||||
rtpv->rtp->used = size + US_RTP_HEADER_SIZE;
|
||||
rtpv->callback(rtpv->rtp);
|
||||
if (frame->used <= _PRE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t fu_overhead = US_RTP_HEADER_SIZE + 2; // FU-A overhead
|
||||
rtpv->rtp->zero_playout_delay = zero_playout_delay;
|
||||
rtpv->rtp->grab_ntp_ts = us_get_now_ntp() - us_ld_to_ntp(us_get_now_monotonic() - frame->grab_begin_ts);
|
||||
|
||||
const uint8_t *src = data + 1;
|
||||
ssize_t remaining = size - 1;
|
||||
const u32 pts = us_get_now_monotonic_u64() * 9 / 100; // PTS units are in 90 kHz
|
||||
|
||||
bool first = true;
|
||||
while (remaining > 0) {
|
||||
ssize_t frag_size = US_RTP_DATAGRAM_SIZE - fu_overhead;
|
||||
const bool last = (remaining <= frag_size);
|
||||
if (last) {
|
||||
frag_size = remaining;
|
||||
}
|
||||
// Bytestream can be like:
|
||||
// [00-]00-00-01 NALU [00-]00-00-01 NALU
|
||||
// ... with optional 00 prefix for _PRE.
|
||||
// We need to iterate by NALUs only and throw away separators.
|
||||
|
||||
us_rtp_write_header(rtpv->rtp, pts, (marked && last));
|
||||
|
||||
DG[US_RTP_HEADER_SIZE] = 28 | (ref_idc << 5);
|
||||
|
||||
uint8_t fu = type;
|
||||
if (first) {
|
||||
fu |= 0x80;
|
||||
}
|
||||
if (last) {
|
||||
fu |= 0x40;
|
||||
}
|
||||
DG[US_RTP_HEADER_SIZE + 1] = fu;
|
||||
|
||||
memcpy(DG + fu_overhead, src, frag_size);
|
||||
rtpv->rtp->used = fu_overhead + frag_size;
|
||||
rtpv->callback(rtpv->rtp);
|
||||
|
||||
src += frag_size;
|
||||
remaining -= frag_size;
|
||||
first = false;
|
||||
const sz offset_to_first = _find_annexb(frame->data, frame->used);
|
||||
if (offset_to_first < 0) {
|
||||
return;
|
||||
}
|
||||
uz begin = offset_to_first + _PRE;
|
||||
|
||||
# undef DG
|
||||
bool first_nalu = true;
|
||||
while (begin < frame->used) { // Find and iterate by NALUs
|
||||
const u8 *const data = frame->data + begin;
|
||||
const sz offset_to_next = _find_annexb(data, frame->used - begin);
|
||||
|
||||
if (offset_to_next >= 0) { // Process NALUs between prefixes
|
||||
uz size = offset_to_next;
|
||||
if (size > 1 && data[size - 1] == 0) { // Check for extra trailing zero
|
||||
--size;
|
||||
}
|
||||
if (size > 1) { // Skip too short NALUs
|
||||
_rtpv_process_nalu(rtpv, data, size, pts, first_nalu, false);
|
||||
first_nalu = false;
|
||||
}
|
||||
begin += offset_to_next + _PRE;
|
||||
|
||||
} else { // Process the tail
|
||||
const uz size = frame->used - begin;
|
||||
if (size > 1) {
|
||||
_rtpv_process_nalu(rtpv, data, size, pts, first_nalu, true);
|
||||
first_nalu = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static ssize_t _find_annexb(const uint8_t *data, size_t size) {
|
||||
static sz _find_annexb(const u8 *data, uz 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;
|
||||
for (uz i = 0; i <= size - _PRE; ++i) {
|
||||
if (
|
||||
data[i] == 0
|
||||
&& data[i + 1] == 0
|
||||
&& data[i + 2] == 1
|
||||
) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,3 +127,78 @@ static ssize_t _find_annexb(const uint8_t *data, size_t size) {
|
||||
}
|
||||
|
||||
#undef _PRE
|
||||
|
||||
static bool _is_nalu_small(uz size) {
|
||||
return (size + US_RTP_HEADER_SIZE <= US_RTP_TOTAL_SIZE);
|
||||
}
|
||||
|
||||
static void _rtpv_process_nalu(us_rtpv_s *rtpv, const u8 *data, uz size, u32 pts, bool first_nalu, bool last_nalu) {
|
||||
US_A(size > 1);
|
||||
if (_is_nalu_small(size)) {
|
||||
_rtpv_process_nalu_small(rtpv, data, size, pts, first_nalu, last_nalu);
|
||||
} else {
|
||||
_rtpv_process_nalu_big(rtpv, data, size, pts, first_nalu, last_nalu);
|
||||
}
|
||||
}
|
||||
|
||||
static void _rtpv_process_nalu_small(us_rtpv_s *rtpv, const u8 *data, uz size, u32 pts, bool first_nalu, bool last_nalu) {
|
||||
US_A(size > 1);
|
||||
US_A(_is_nalu_small(size));
|
||||
|
||||
us_rtp_s *rtp = rtpv->rtp;
|
||||
u8 *dg = rtp->datagram;
|
||||
|
||||
us_rtp_write_header(rtp, pts, last_nalu);
|
||||
memcpy(dg + US_RTP_HEADER_SIZE, data, size);
|
||||
rtp->used = size + US_RTP_HEADER_SIZE;
|
||||
rtp->first_of_frame = first_nalu;
|
||||
rtp->last_of_frame = last_nalu;
|
||||
rtpv->callback(rtp);
|
||||
}
|
||||
|
||||
static void _rtpv_process_nalu_big(us_rtpv_s *rtpv, const u8 *data, uz size, u32 pts, bool first_nalu, bool last_nalu) {
|
||||
US_A(size > 1);
|
||||
US_A(!_is_nalu_small(size));
|
||||
|
||||
const uint ref_idc = (data[0] >> 5) & 3;
|
||||
const uint type = data[0] & 0x1F;
|
||||
us_rtp_s *rtp = rtpv->rtp;
|
||||
u8 *dg = rtp->datagram;
|
||||
|
||||
const uz fu_overhead = US_RTP_HEADER_SIZE + 2; // FU-A overhead
|
||||
|
||||
const u8 *src = data + 1;
|
||||
sz remaining = size - 1;
|
||||
|
||||
bool first = true;
|
||||
while (remaining > 0) {
|
||||
sz frag_size = US_RTP_TOTAL_SIZE - fu_overhead;
|
||||
const bool last = (remaining <= frag_size);
|
||||
if (last) {
|
||||
frag_size = remaining;
|
||||
}
|
||||
|
||||
us_rtp_write_header(rtp, pts, (last_nalu && last));
|
||||
|
||||
dg[US_RTP_HEADER_SIZE] = 28 | (ref_idc << 5);
|
||||
|
||||
u8 fu = type;
|
||||
if (first) {
|
||||
fu |= 0x80;
|
||||
}
|
||||
if (last) {
|
||||
fu |= 0x40;
|
||||
}
|
||||
dg[US_RTP_HEADER_SIZE + 1] = fu;
|
||||
|
||||
memcpy(dg + fu_overhead, src, frag_size);
|
||||
rtp->used = fu_overhead + frag_size;
|
||||
rtp->first_of_frame = (first_nalu && first);
|
||||
rtp->last_of_frame = (last_nalu && last);
|
||||
rtpv->callback(rtp);
|
||||
|
||||
src += frag_size;
|
||||
remaining -= frag_size;
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,16 +22,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <inttypes.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#include "uslibs/tools.h"
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/frame.h"
|
||||
|
||||
#include "rtp.h"
|
||||
@@ -46,5 +37,4 @@ typedef struct {
|
||||
us_rtpv_s *us_rtpv_init(us_rtp_callback_f callback);
|
||||
void us_rtpv_destroy(us_rtpv_s *rtpv);
|
||||
|
||||
char *us_rtpv_make_sdp(us_rtpv_s *rtpv);
|
||||
void us_rtpv_wrap(us_rtpv_s *rtpv, const us_frame_s *frame, bool zero_playout_delay);
|
||||
|
||||
119
janus/src/sdp.c
Normal file
119
janus/src/sdp.c
Normal file
@@ -0,0 +1,119 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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 "sdp.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <janus/plugins/plugin.h>
|
||||
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/tools.h"
|
||||
|
||||
#include "rtp.h"
|
||||
#include "rtpv.h"
|
||||
#include "rtpa.h"
|
||||
|
||||
|
||||
char *us_sdp_create(u32 video_ssrc, u32 audio_ssrc, bool acap, bool aplay, bool vplay) {
|
||||
char *video_sdp;
|
||||
{
|
||||
// https://tools.ietf.org/html/rfc6184
|
||||
// https://github.com/meetecho/janus-gateway/issues/2443
|
||||
const uint pl = US_RTP_H264_PAYLOAD;
|
||||
US_ASPRINTF(
|
||||
video_sdp,
|
||||
"m=video 1 RTP/SAVPF %u" RN
|
||||
"c=IN IP4 0.0.0.0" RN
|
||||
"a=rtpmap:%u H264/90000" RN
|
||||
"a=fmtp:%u profile-level-id=42E01F;packetization-mode=1" RN
|
||||
"a=rtcp-fb:%u ccm fir" RN
|
||||
"a=rtcp-fb:%u nack" RN
|
||||
"a=rtcp-fb:%u nack pli" RN
|
||||
"a=rtcp-fb:%u goog-remb" RN
|
||||
"a=mid:v" RN
|
||||
"a=msid:video v" RN
|
||||
"a=ssrc:%" PRIu32 " cname:ustreamer" RN
|
||||
"a=extmap:1/sendonly urn:3gpp:video-orientation" RN
|
||||
"a=extmap:2/sendonly http://www.webrtc.org/experiments/rtp-hdrext/playout-delay" RN
|
||||
"a=extmap:3/sendonly http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time" RN
|
||||
"a=%s" RN,
|
||||
pl, pl, pl, pl, pl, pl, pl,
|
||||
video_ssrc,
|
||||
(vplay ? "sendrecv" : "sendonly"));
|
||||
}
|
||||
|
||||
char *audio_sdp;
|
||||
if (acap || aplay) {
|
||||
const uint pl = US_RTP_OPUS_PAYLOAD;
|
||||
|
||||
char *dir = "";
|
||||
if (acap && aplay) {
|
||||
dir = "sendrecv";
|
||||
} else if (acap && !aplay) {
|
||||
dir = "sendonly";
|
||||
} else if (!acap && aplay) {
|
||||
dir = "recvonly";
|
||||
}
|
||||
|
||||
US_ASPRINTF(
|
||||
audio_sdp,
|
||||
"m=audio 1 RTP/SAVPF %u" RN
|
||||
"c=IN IP4 0.0.0.0" RN
|
||||
"a=rtpmap:%u OPUS/%u/%u" RN
|
||||
"a=fmtp:%u sprop-stereo=1" RN // useinbandfec=1
|
||||
"a=mid:a" RN
|
||||
"a=msid:audio a" RN
|
||||
"a=ssrc:%" PRIu32 " cname:ustreamer" RN
|
||||
"a=%s" RN,
|
||||
pl, pl,
|
||||
US_RTP_OPUS_HZ, US_RTP_OPUS_CH,
|
||||
pl,
|
||||
audio_ssrc,
|
||||
dir);
|
||||
} else {
|
||||
audio_sdp = us_strdup("");
|
||||
}
|
||||
|
||||
char *sdp;
|
||||
US_ASPRINTF(sdp,
|
||||
"v=0" RN
|
||||
"o=- %" PRIu64 " 1 IN IP4 0.0.0.0" RN
|
||||
"s=PiKVM uStreamer" RN
|
||||
"t=0 0" RN
|
||||
"%s%s",
|
||||
us_get_now_id() >> 1,
|
||||
# if JANUS_PLUGIN_API_VERSION >= 100
|
||||
// Place video SDP before audio SDP so that the video and audio streams
|
||||
// have predictable indices, even if audio is not available.
|
||||
// See also client.c.
|
||||
video_sdp, audio_sdp
|
||||
# else
|
||||
// For versions of Janus prior to 1.x, place the audio SDP first.
|
||||
audio_sdp, video_sdp
|
||||
# endif
|
||||
);
|
||||
|
||||
free(audio_sdp);
|
||||
free(video_sdp);
|
||||
return sdp;
|
||||
}
|
||||
28
janus/src/sdp.h
Normal file
28
janus/src/sdp.h
Normal file
@@ -0,0 +1,28 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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 "uslibs/types.h"
|
||||
|
||||
|
||||
char *us_sdp_create(u32 video_ssrc, u32 audio_ssrc, bool acap, bool aplay, bool vplay);
|
||||
1
janus/src/uslibs/chip.c
Symbolic link
1
janus/src/uslibs/chip.c
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/chip.c
|
||||
1
janus/src/uslibs/chip.h
Symbolic link
1
janus/src/uslibs/chip.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/chip.h
|
||||
1
janus/src/uslibs/errors.h
Symbolic link
1
janus/src/uslibs/errors.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/errors.h
|
||||
1
janus/src/uslibs/logging.c
Symbolic link
1
janus/src/uslibs/logging.c
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/logging.c
|
||||
1
janus/src/uslibs/logging.h
Symbolic link
1
janus/src/uslibs/logging.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/logging.h
|
||||
32
janus/src/uslibs/logging_base.h
Normal file
32
janus/src/uslibs/logging_base.h
Normal file
@@ -0,0 +1,32 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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 <janus/plugins/plugin.h>
|
||||
|
||||
#include "threading.h"
|
||||
|
||||
|
||||
#define US_LOG_PRINTF_NOLOCK(x_label_color, x_label, x_msg_color, x_msg, ...) { \
|
||||
char m_tname_buf[US_THREAD_NAME_SIZE] = {0}; \
|
||||
us_thread_get_name(m_tname_buf); \
|
||||
JANUS_LOG(LOG_INFO, "== ustreamer/%-15s " x_label " -- " x_msg "\n", m_tname_buf, ##__VA_ARGS__); \
|
||||
}
|
||||
1
janus/src/uslibs/memsink.c
Symbolic link
1
janus/src/uslibs/memsink.c
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/memsink.c
|
||||
1
janus/src/uslibs/memsink.h
Symbolic link
1
janus/src/uslibs/memsink.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/memsink.h
|
||||
1
janus/src/uslibs/memsinksh.c
Symbolic link
1
janus/src/uslibs/memsinksh.c
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/memsinksh.c
|
||||
1
janus/src/uslibs/queue.c
Symbolic link
1
janus/src/uslibs/queue.c
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/queue.c
|
||||
1
janus/src/uslibs/queue.h
Symbolic link
1
janus/src/uslibs/queue.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/queue.h
|
||||
1
janus/src/uslibs/ring.c
Symbolic link
1
janus/src/uslibs/ring.c
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/ring.c
|
||||
1
janus/src/uslibs/ring.h
Symbolic link
1
janus/src/uslibs/ring.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/ring.h
|
||||
1
janus/src/uslibs/types.h
Symbolic link
1
janus/src/uslibs/types.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/types.h
|
||||
@@ -9,7 +9,7 @@ RUN pacman -Syu --noconfirm \
|
||||
&& pacman -S --needed --noconfirm \
|
||||
vim \
|
||||
git \
|
||||
libjpeg \
|
||||
libjpeg-turbo \
|
||||
libevent \
|
||||
libutil-linux \
|
||||
libbsd \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[mypy]
|
||||
python_version = 3.9
|
||||
python_version = 3.14
|
||||
ignore_missing_imports = true
|
||||
disallow_untyped_defs = true
|
||||
strict_optional = true
|
||||
|
||||
@@ -33,7 +33,7 @@ max-line-length = 160
|
||||
|
||||
[BASIC]
|
||||
# Good variable names which should always be accepted, separated by a comma
|
||||
good-names = _, __, x, y, ws, make-html-h, make-jpeg-h, make-ico-h
|
||||
good-names = _, __, i, x, y, ws, make-html-h, make-ico-h
|
||||
|
||||
# Regular expression matching correct method names
|
||||
method-rgx = [a-z_][a-z0-9_]{2,50}$
|
||||
|
||||
@@ -3,7 +3,7 @@ envlist = cppcheck, flake8, pylint, mypy, vulture, htmlhint
|
||||
skipsdist = true
|
||||
|
||||
[testenv]
|
||||
basepython = python3.11
|
||||
basepython = python3.14
|
||||
changedir = /src
|
||||
|
||||
[testenv:cppcheck]
|
||||
@@ -13,19 +13,19 @@ commands = cppcheck \
|
||||
--std=c17 \
|
||||
--error-exitcode=1 \
|
||||
--quiet \
|
||||
--enable=warning,unusedFunction,portability,performance,style \
|
||||
--suppress=assignmentInAssert \
|
||||
--check-level=exhaustive \
|
||||
--enable=warning,portability,performance,style \
|
||||
--suppress=variableScope \
|
||||
--inline-suppr \
|
||||
--library=python \
|
||||
--include=linters/cppcheck.h \
|
||||
src python/*.? janus/*.?
|
||||
src python/src/*.? janus/src/*.?
|
||||
|
||||
[testenv:flake8]
|
||||
allowlist_externals = bash
|
||||
commands = bash -c 'flake8 --config=linters/flake8.ini tools/*.py' python/*.py
|
||||
deps =
|
||||
flake8==5.0.4
|
||||
flake8
|
||||
flake8-quotes
|
||||
|
||||
[testenv:pylint]
|
||||
@@ -33,10 +33,11 @@ allowlist_externals = bash
|
||||
commands = bash -c 'pylint --rcfile=linters/pylint.ini --output-format=colorized --reports=no tools/*.py python/*.py'
|
||||
deps =
|
||||
pylint
|
||||
setuptools
|
||||
|
||||
[testenv:mypy]
|
||||
allowlist_externals = bash
|
||||
commands = bash -c 'mypy --config-file=linters/mypy.ini tools/*.py python/*.py'
|
||||
commands = bash -c 'mypy --config-file=linters/mypy.ini --cache-dir=/dev/null tools/*.py python/*.py'
|
||||
deps =
|
||||
mypy
|
||||
|
||||
|
||||
@@ -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 5.46" "January 2021"
|
||||
.TH USTREAMER-DUMP 1 "version 6.64" "January 2021"
|
||||
|
||||
.SH NAME
|
||||
ustreamer-dump \- Dump uStreamer's memory sink to file
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Manpage for ustreamer.
|
||||
.\" Open an issue or pull request to https://github.com/pikvm/ustreamer to correct errors or typos
|
||||
.TH USTREAMER 1 "version 5.46" "November 2020"
|
||||
.TH USTREAMER 1 "version 6.64" "November 2020"
|
||||
|
||||
.SH NAME
|
||||
ustreamer \- stream MJPEG video from any V4L2 device to the network
|
||||
@@ -17,17 +17,17 @@ Without arguments, \fBustreamer\fR will try to open \fB/dev/video0\fR with 640x4
|
||||
|
||||
Please note that since µStreamer v2\.0 cross\-domain requests were disabled by default for security reasons\. To enable the old behavior, use the option \fB\-\-allow\-origin=\e*\fR\.
|
||||
|
||||
For example, the recommended way of running µStreamer with Auvidea B101 on a Raspberry Pi is:
|
||||
For example, the recommended way of running µStreamer with TC358743-based capture device on a Raspberry Pi is:
|
||||
|
||||
\fBustreamer \e\fR
|
||||
.RS
|
||||
\fB\-\-format=uyvy \e\fR # Device input format
|
||||
.nf
|
||||
\fB\-\-encoder=m2m-image \e\fR # Hardware encoding with V4L2 M2M intraface
|
||||
\fB\-\-encoder=m2m-image \e\fR # Hardware encoding with V4L2 M2M interface
|
||||
.nf
|
||||
\fB\-\-workers=3 \e\fR # Maximum workers for V4L2 encoder
|
||||
.nf
|
||||
\fB\-\-persistent \e\fR # Don\'t re\-initialize device on timeout (for example when HDMI cable was disconnected)
|
||||
\fB\-\-persistent \e\fR # Suppress repetitive signal source errors (for example when HDMI cable was disconnected)
|
||||
.nf
|
||||
\fB\-\-dv\-timings \e\fR # Use DV\-timings
|
||||
.nf
|
||||
@@ -52,7 +52,7 @@ Initial image resolution. Default: 640x480.
|
||||
.TP
|
||||
.BR \-m\ \fIfmt ", " \-\-format\ \fIfmt
|
||||
Image format.
|
||||
Available: YUYV, UYVY, RGB565, RGB24, JPEG; default: YUYV.
|
||||
Available: YUYV, YVYU, UYVY, YUV420, YVU420, RGB565, RGB24, GREY, MJPEG, JPEG; default: YUYV.
|
||||
.TP
|
||||
.BR \-a\ \fIstd ", " \-\-tv\-standard\ \fIstd
|
||||
Force TV standard.
|
||||
@@ -66,16 +66,19 @@ Available: MMAP, USERPTR; default: MMAP.
|
||||
Desired FPS. Default: maximum possible.
|
||||
.TP
|
||||
.BR \-z\ \fIN ", " \-\-min\-frame\-size\ \fIN
|
||||
Drop frames smaller then this limit. Useful if the device produces small\-sized garbage frames. Default: 128 bytes.
|
||||
Drop frames smaller than this limit. Useful if the device produces small\-sized garbage frames. Default: 128 bytes.
|
||||
.TP
|
||||
.BR \-T ", " \-\-allow\-truncated\-frames
|
||||
Allows to handle truncated frames. Useful if the device produces incorrect but still acceptable frames. Default: disabled.
|
||||
.TP
|
||||
.BR \-n ", " \-\-persistent
|
||||
Don't re\-initialize device on timeout. Default: disabled.
|
||||
Suppress repetitive signal source errors. Default: disabled.
|
||||
.TP
|
||||
.BR \-t ", " \-\-dv\-timings
|
||||
Enable DV-timings querying and events processing to automatic resolution change. Default: disabled.
|
||||
.TP
|
||||
.BR \-b\ \fIN ", " \-\-buffers\ \fIN
|
||||
The number of buffers to receive data from the device. Each buffer may processed using an independent thread.
|
||||
The number of buffers to receive data from the device. Each buffer may be processed using an independent thread.
|
||||
Default: 2 (the number of CPU cores (but not more than 4) + 1).
|
||||
.TP
|
||||
.BR \-w\ \fIN ", " \-\-workers\ \fIN
|
||||
@@ -96,17 +99,15 @@ HW ─ Use pre-encoded MJPEG frames directly from camera hardware.
|
||||
M2M-VIDEO ─ GPU-accelerated MJPEG encoding.
|
||||
|
||||
M2M-IMAGE ─ GPU-accelerated JPEG encoding.
|
||||
|
||||
NOOP ─ Don't compress MJPEG stream (do nothing).
|
||||
.TP
|
||||
.BR \-g\ \fIWxH,... ", " \-\-glitched\-resolutions\ \fIWxH,...
|
||||
It doesn't do anything. Still here for compatibility.
|
||||
.TP
|
||||
.BR \-k\ \fIpath ", " \-\-blank\ \fIpath
|
||||
Path to JPEG file that will be shown when the device is disconnected during the streaming. Default: black screen 640x480 with 'NO SIGNAL'.
|
||||
It doesn't do anything. Still here for compatibility.
|
||||
.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. Note: currently this option has no effect on memory sinks. Default: disabled.
|
||||
It doesn't do anything. Still here for compatibility.
|
||||
.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.
|
||||
@@ -119,6 +120,12 @@ Delay before trying to connect to the device again after an error (timeout for e
|
||||
.TP
|
||||
.BR \-\-m2m\-device\ \fI/dev/path
|
||||
Path to V4L2 mem-to-mem encoder device. Default: auto-select.
|
||||
.TP
|
||||
.BR \-\-media\-device \fI/dev/path
|
||||
Path to V4L2 /dev/media* device for setting subdevices (currently necessary for Raspberry Pi 5). Default: unset.
|
||||
.TP
|
||||
.BR \-\-media\-entity\-name \fIname
|
||||
Name of the V4L2 entity to grab video from, will be router to \-\-device, for example "tc358743 10-000f". Default: unset.
|
||||
|
||||
.SS "Image control options"
|
||||
.TP
|
||||
@@ -212,25 +219,25 @@ Timeout for client connections. Default: 10.
|
||||
.SS "JPEG sink options"
|
||||
With shared memory sink you can write a stream to a file. See \fBustreamer-dump\fR(1) for more info.
|
||||
.TP
|
||||
.BR \-\-sink\ \fIname
|
||||
Use the specified shared memory object to sink JPEG frames. Default: disabled.
|
||||
.BR \-\-jpeg\-sink\ \fIname
|
||||
Use the specified shared memory object to sink JPEG frames. The name should end with a suffix ".jpeg" or ":jpeg". Default: disabled.
|
||||
.TP
|
||||
.BR \-\-sink\-mode\ \fImode
|
||||
.BR \-\-jpeg\-sink\-mode\ \fImode
|
||||
Set JPEG sink permissions (like 777). Default: 660.
|
||||
.TP
|
||||
.BR \-\-sink\-rm
|
||||
.BR \-\-jpeg\-sink\-rm
|
||||
Remove shared memory on stop. Default: disabled.
|
||||
.TP
|
||||
.BR \-\-sink\-client\-ttl\ \fIsec
|
||||
.BR \-\-jpeg\-sink\-client\-ttl\ \fIsec
|
||||
Client TTL. Default: 10.
|
||||
.TP
|
||||
.BR \-\-sink\-timeout\ \fIsec
|
||||
.BR \-\-jpeg\-sink\-timeout\ \fIsec
|
||||
Timeout for lock. Default: 1.
|
||||
|
||||
.SS "H264 sink options"
|
||||
.TP
|
||||
.BR \-\-h264\-sink\ \fIname
|
||||
Use the specified shared memory object to sink H264 frames. Default: disabled.
|
||||
Use the specified shared memory object to sink H264 frames. The name should end with a suffix ".h264" or ":h264". Default: disabled.
|
||||
.TP
|
||||
.BR \-\-h264\-sink\-mode\ \fImode
|
||||
Set H264 sink permissions (like 777). Default: 660.
|
||||
@@ -252,12 +259,34 @@ Interval between keyframes. Default: 30.
|
||||
.TP
|
||||
.BR \-\-h264\-m2m\-device\ \fI/dev/path
|
||||
Path to V4L2 mem-to-mem encoder device. Default: auto-select.
|
||||
.TP
|
||||
.BR \-\-h264\-boost\-device
|
||||
Increase encoder performance on PiKVM V4. Default: disabled.
|
||||
|
||||
.SS "RAW sink options"
|
||||
.TP
|
||||
.BR \-\-raw\-sink\ \fIname
|
||||
Use the specified shared memory object to sink RAW frames. The name should end with a suffix ".raw" or ":raw". Default: disabled.
|
||||
.TP
|
||||
.BR \-\-raw\-sink\-mode\ \fImode
|
||||
Set RAW sink permissions (like 777). Default: 660.
|
||||
.TP
|
||||
.BR \-\-raw\-sink\-rm
|
||||
Remove shared memory on stop. Default: disabled.
|
||||
.TP
|
||||
.BR \-\-raw\-sink\-client\-ttl\ \fIsec
|
||||
Client TTL. Default: 10.
|
||||
.TP
|
||||
.BR \-\-raw\-sink\-timeout\ \fIsec
|
||||
Timeout for lock. Default: 1.
|
||||
|
||||
.SS "Process options"
|
||||
.TP
|
||||
.BR \-\-exit\-on\-parent\-death
|
||||
Exit the program if the parent process is dead. Required \fBHAS_PDEATHSIG\fR feature. Default: disabled.
|
||||
Exit the program if the parent process is dead. Required \fBWITH_PDEATHSIG\fR feature. Default: disabled.
|
||||
.TP
|
||||
.BR \-\-exit\-on\-device\-error
|
||||
Exit on any device error instead of polling until success. Default: disabled.
|
||||
.TP
|
||||
.BR \-\-exit\-on\-no\-clients \fIsec
|
||||
Exit the program if there have been no stream or sink clients or any HTTP requests in the last N seconds. Default: 0 (disabled).
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
|
||||
|
||||
pkgname=ustreamer
|
||||
pkgver=5.46
|
||||
pkgver=6.64
|
||||
pkgrel=1
|
||||
pkgdesc="Lightweight and fast MJPEG-HTTP streamer"
|
||||
url="https://github.com/pikvm/ustreamer"
|
||||
license=(GPL)
|
||||
arch=(i686 x86_64 armv6h armv7h aarch64)
|
||||
depends=(libjpeg libevent libbsd libgpiod systemd)
|
||||
makedepends=(gcc make systemd)
|
||||
depends=(libjpeg-turbo libevent libbsd libgpiod systemd)
|
||||
makedepends=(gcc make pkgconf systemd)
|
||||
source=(${pkgname}::"git+https://github.com/pikvm/ustreamer#commit=v${pkgver}")
|
||||
md5sums=(SKIP)
|
||||
|
||||
@@ -18,8 +18,8 @@ md5sums=(SKIP)
|
||||
_options="WITH_GPIO=1 WITH_SYSTEMD=1"
|
||||
if [ -e /usr/bin/python3 ]; then
|
||||
_options="$_options WITH_PYTHON=1"
|
||||
depends+=(python)
|
||||
makedepends+=(python-setuptools)
|
||||
depends+=("python>=3.14" "python<3.15")
|
||||
makedepends+=(python-setuptools python-pip python-build python-wheel python-installer)
|
||||
fi
|
||||
if [ -e /usr/include/janus/plugins/plugin.h ];then
|
||||
depends+=(janus-gateway alsa-lib opus)
|
||||
@@ -28,12 +28,6 @@ if [ -e /usr/include/janus/plugins/plugin.h ];then
|
||||
fi
|
||||
|
||||
|
||||
# LD does not link mmal with this option
|
||||
# This DOESN'T affect setup.py
|
||||
LDFLAGS="${LDFLAGS//--as-needed/}"
|
||||
export LDFLAGS="${LDFLAGS//,,/,}"
|
||||
|
||||
|
||||
build() {
|
||||
cd "$srcdir"
|
||||
rm -rf $pkgname-build
|
||||
|
||||
@@ -24,7 +24,7 @@ RUN apk add --no-cache \
|
||||
WORKDIR /ustreamer
|
||||
COPY --from=build /build/ustreamer/src/ustreamer.bin ustreamer
|
||||
|
||||
RUN wget https://raw.githubusercontent.com/pikvm/kvmd/master/configs/kvmd/edid/v3-hdmi.hex -O /edid.hex
|
||||
RUN wget https://raw.githubusercontent.com/pikvm/kvmd/master/configs/kvmd/edid/v2.hex -O /edid.hex
|
||||
COPY pkg/docker/entry.sh /
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
# This is free software, licensed under the GNU General Public License v2.
|
||||
# See /LICENSE for more information.
|
||||
#
|
||||
# This package is just an example. For OpenWRT it is recommended to use upstream package:
|
||||
# - https://github.com/openwrt/packages/tree/master/multimedia/ustreamer
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=ustreamer
|
||||
PKG_VERSION:=5.46
|
||||
PKG_VERSION:=6.64
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=Maxim Devaev <mdevaev@gmail.com>
|
||||
|
||||
@@ -25,7 +28,7 @@ define Package/ustreamer
|
||||
SECTION:=multimedia
|
||||
CATEGORY:=Multimedia
|
||||
TITLE:=uStreamer
|
||||
DEPENDS:=+libpthread +libjpeg +libv4l +libbsd +libevent2 +libevent2-core +libevent2-extra +libevent2-pthreads
|
||||
DEPENDS:=+libatomic +libpthread +libjpeg +libv4l +libbsd +libevent2 +libevent2-core +libevent2-extra +libevent2-pthreads
|
||||
URL:=https://github.com/pikvm/ustreamer
|
||||
endef
|
||||
|
||||
|
||||
2
python/MANIFEST.in
Normal file
2
python/MANIFEST.in
Normal file
@@ -0,0 +1,2 @@
|
||||
include setup.py
|
||||
recursive-include src *.c *h
|
||||
@@ -1,20 +1,20 @@
|
||||
-include ../config.mk
|
||||
|
||||
DESTDIR ?=
|
||||
R_DESTDIR ?=
|
||||
PREFIX ?= /usr/local
|
||||
|
||||
PY ?= python3
|
||||
|
||||
|
||||
# =====
|
||||
all:
|
||||
all: root
|
||||
root: $(shell find src -type f,l) setup.py
|
||||
$(info == PY_BUILD ustreamer-*.so)
|
||||
@ $(PY) setup.py build
|
||||
rm -rf root
|
||||
$(ECHO) $(PY) -m build --wheel --skip-dependency-check --no-isolation
|
||||
|
||||
|
||||
install:
|
||||
$(PY) setup.py install --prefix=$(PREFIX) --root=$(if $(DESTDIR),$(DESTDIR),/)
|
||||
$(PY) -m installer --prefix=$(PREFIX) $(if $(R_DESTDIR),--destdir=$(R_DESTDIR),) dist/*.whl
|
||||
|
||||
|
||||
clean:
|
||||
rm -rf build ustreamer.egg-info
|
||||
rm -rf root dist build ustreamer.egg-info
|
||||
|
||||
@@ -5,19 +5,36 @@ from setuptools import setup
|
||||
|
||||
|
||||
# =====
|
||||
def _find_sources(suffix: str) -> list[str]:
|
||||
def _find_sources() -> list[str]:
|
||||
sources: list[str] = []
|
||||
for (root_path, _, names) in os.walk("src"):
|
||||
for name in names:
|
||||
if name.endswith(suffix):
|
||||
if name.endswith(".c"):
|
||||
sources.append(os.path.join(root_path, name))
|
||||
return sources
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
def _find_flags() -> dict[str, bool]:
|
||||
return {
|
||||
key[3:]: (value.strip().lower() in ["true", "on", "1"])
|
||||
for (key, value) in sorted(os.environ.items())
|
||||
if key.startswith("MK_WITH_")
|
||||
}
|
||||
|
||||
|
||||
def _make_d_features(flags: dict[str, bool]) -> str:
|
||||
features = " ".join([
|
||||
f"{key}={int(value)}"
|
||||
for (key, value) in flags.items()
|
||||
])
|
||||
return f"-DUS_FEATURES=\"{features}\""
|
||||
|
||||
|
||||
def main() -> None:
|
||||
flags = _find_flags()
|
||||
setup(
|
||||
name="ustreamer",
|
||||
version="5.46",
|
||||
version="6.64",
|
||||
description="uStreamer tools",
|
||||
author="Maxim Devaev",
|
||||
author_email="mdevaev@gmail.com",
|
||||
@@ -26,10 +43,16 @@ if __name__ == "__main__":
|
||||
Extension(
|
||||
"ustreamer",
|
||||
libraries=["rt", "m", "pthread"],
|
||||
extra_compile_args=["-std=c17", "-D_GNU_SOURCE"],
|
||||
extra_compile_args=[
|
||||
"-std=c17", "-D_GNU_SOURCE",
|
||||
_make_d_features(flags),
|
||||
],
|
||||
undef_macros=["NDEBUG"],
|
||||
sources=_find_sources(".c"),
|
||||
depends=_find_sources(".h"),
|
||||
sources=_find_sources(),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
1
python/src/uslibs/const.h
Symbolic link
1
python/src/uslibs/const.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/const.h
|
||||
1
python/src/uslibs/errors.h
Symbolic link
1
python/src/uslibs/errors.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/errors.h
|
||||
1
python/src/uslibs/memsinksh.c
Symbolic link
1
python/src/uslibs/memsinksh.c
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/memsinksh.c
|
||||
1
python/src/uslibs/types.h
Symbolic link
1
python/src/uslibs/types.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/types.h
|
||||
@@ -1,3 +1,25 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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 <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
@@ -13,6 +35,9 @@
|
||||
|
||||
#include <Python.h>
|
||||
|
||||
#include "uslibs/const.h"
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/errors.h"
|
||||
#include "uslibs/tools.h"
|
||||
#include "uslibs/frame.h"
|
||||
#include "uslibs/memsinksh.h"
|
||||
@@ -25,36 +50,29 @@ typedef struct {
|
||||
double lock_timeout;
|
||||
double wait_timeout;
|
||||
double drop_same_frames;
|
||||
uz data_size;
|
||||
|
||||
int fd;
|
||||
us_memsink_shared_s *mem;
|
||||
|
||||
uint64_t frame_id;
|
||||
long double frame_ts;
|
||||
u64 frame_id;
|
||||
ldf frame_ts;
|
||||
us_frame_s *frame;
|
||||
} _MemsinkObject;
|
||||
|
||||
|
||||
#define _MEM(x_next) self->mem->x_next
|
||||
#define _FRAME(x_next) self->frame->x_next
|
||||
|
||||
|
||||
static void _MemsinkObject_destroy_internals(_MemsinkObject *self) {
|
||||
if (self->mem != NULL) {
|
||||
us_memsink_shared_unmap(self->mem);
|
||||
us_memsinksh_unmap(self->mem, self->data_size);
|
||||
self->mem = NULL;
|
||||
}
|
||||
if (self->fd >= 0) {
|
||||
close(self->fd);
|
||||
self->fd = -1;
|
||||
}
|
||||
if (self->frame != NULL) {
|
||||
us_frame_destroy(self->frame);
|
||||
self->frame = NULL;
|
||||
}
|
||||
US_CLOSE_FD(self->fd);
|
||||
US_DELETE(self->frame, us_frame_destroy);
|
||||
}
|
||||
|
||||
static int _MemsinkObject_init(_MemsinkObject *self, PyObject *args, PyObject *kwargs) {
|
||||
self->fd = -1;
|
||||
|
||||
self->lock_timeout = 1;
|
||||
self->wait_timeout = 1;
|
||||
|
||||
@@ -65,41 +83,42 @@ static int _MemsinkObject_init(_MemsinkObject *self, PyObject *args, PyObject *k
|
||||
return -1;
|
||||
}
|
||||
|
||||
# define SET_DOUBLE(_field, _cond) { \
|
||||
if (!(self->_field _cond)) { \
|
||||
PyErr_SetString(PyExc_ValueError, #_field " must be " #_cond); \
|
||||
# define SET_DOUBLE(x_field, x_cond) { \
|
||||
if (!(self->x_field x_cond)) { \
|
||||
PyErr_SetString(PyExc_ValueError, #x_field " must be " #x_cond); \
|
||||
return -1; \
|
||||
} \
|
||||
}
|
||||
|
||||
SET_DOUBLE(lock_timeout, > 0);
|
||||
SET_DOUBLE(wait_timeout, > 0);
|
||||
SET_DOUBLE(drop_same_frames, >= 0);
|
||||
|
||||
# undef SET_DOUBLE
|
||||
|
||||
if ((self->data_size = us_memsinksh_calculate_size(self->obj)) == 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "Invalid memsink object suffix");
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->frame = us_frame_init();
|
||||
|
||||
if ((self->fd = shm_open(self->obj, O_RDWR, 0)) == -1) {
|
||||
PyErr_SetFromErrno(PyExc_OSError);
|
||||
goto error;
|
||||
}
|
||||
|
||||
if ((self->mem = us_memsink_shared_map(self->fd)) == NULL) {
|
||||
if ((self->mem = us_memsinksh_map(self->fd, self->data_size)) == NULL) {
|
||||
PyErr_SetFromErrno(PyExc_OSError);
|
||||
goto error;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
error:
|
||||
_MemsinkObject_destroy_internals(self);
|
||||
return -1;
|
||||
error:
|
||||
_MemsinkObject_destroy_internals(self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static PyObject *_MemsinkObject_repr(_MemsinkObject *self) {
|
||||
char repr[1024];
|
||||
snprintf(repr, 1023, "<Memsink(%s)>", self->obj);
|
||||
US_SNPRINTF(repr, 1023, "<Memsink(%s)>", self->obj);
|
||||
return Py_BuildValue("s", repr);
|
||||
}
|
||||
|
||||
@@ -115,70 +134,77 @@ static PyObject *_MemsinkObject_close(_MemsinkObject *self, PyObject *Py_UNUSED(
|
||||
|
||||
static PyObject *_MemsinkObject_enter(_MemsinkObject *self, PyObject *Py_UNUSED(ignored)) {
|
||||
Py_INCREF(self);
|
||||
return (PyObject *)self;
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
static PyObject *_MemsinkObject_exit(_MemsinkObject *self, PyObject *Py_UNUSED(ignored)) {
|
||||
return PyObject_CallMethod((PyObject *)self, "close", "");
|
||||
return PyObject_CallMethod((PyObject*)self, "close", "");
|
||||
}
|
||||
|
||||
static int _wait_frame(_MemsinkObject *self) {
|
||||
const long double deadline_ts = us_get_now_monotonic() + self->wait_timeout;
|
||||
const ldf deadline_ts = us_get_now_monotonic() + self->wait_timeout;
|
||||
|
||||
# define RETURN_OS_ERROR { \
|
||||
Py_BLOCK_THREADS \
|
||||
PyErr_SetFromErrno(PyExc_OSError); \
|
||||
return -1; \
|
||||
}
|
||||
|
||||
long double now;
|
||||
int locked = -1;
|
||||
ldf now_ts;
|
||||
do {
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
|
||||
const int retval = us_flock_timedwait_monotonic(self->fd, self->lock_timeout);
|
||||
now = us_get_now_monotonic();
|
||||
|
||||
if (retval < 0 && errno != EWOULDBLOCK) {
|
||||
RETURN_OS_ERROR;
|
||||
|
||||
} else if (retval == 0) {
|
||||
if (_MEM(magic) == US_MEMSINK_MAGIC && _MEM(version) == US_MEMSINK_VERSION && _MEM(id) != self->frame_id) {
|
||||
if (self->drop_same_frames > 0) {
|
||||
if (
|
||||
US_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;
|
||||
locked = us_flock_timedwait_monotonic(self->fd, self->lock_timeout);
|
||||
now_ts = us_get_now_monotonic();
|
||||
if (locked < 0) {
|
||||
if (errno == EWOULDBLOCK) {
|
||||
goto retry;
|
||||
}
|
||||
goto os_error;
|
||||
}
|
||||
|
||||
if (flock(self->fd, LOCK_UN) < 0) {
|
||||
RETURN_OS_ERROR;
|
||||
us_memsink_shared_s *mem = self->mem;
|
||||
if (mem->magic != US_MEMSINK_MAGIC || mem->version != US_MEMSINK_VERSION) {
|
||||
goto retry;
|
||||
}
|
||||
|
||||
// Let the sink know that the client is alive
|
||||
mem->last_client_ts = now_ts;
|
||||
mem->client_magic = US_MEMSINK_MAGIC;
|
||||
|
||||
if (mem->id == self->frame_id) {
|
||||
goto retry;
|
||||
}
|
||||
|
||||
if (self->drop_same_frames > 0) {
|
||||
if (
|
||||
US_FRAME_COMPARE_GEOMETRY(self->mem, self->frame)
|
||||
&& (self->frame_ts + self->drop_same_frames > now_ts)
|
||||
&& !memcmp(self->frame->data, us_memsinksh_get_data(mem), mem->used)
|
||||
) {
|
||||
self->frame_id = mem->id;
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
|
||||
drop:
|
||||
// New frame found
|
||||
Py_BLOCK_THREADS
|
||||
return 0;
|
||||
|
||||
os_error:
|
||||
Py_BLOCK_THREADS
|
||||
PyErr_SetFromErrno(PyExc_OSError);
|
||||
return -1;
|
||||
|
||||
retry:
|
||||
if (locked >= 0 && flock(self->fd, LOCK_UN) < 0) {
|
||||
goto os_error;
|
||||
}
|
||||
if (usleep(1000) < 0) {
|
||||
RETURN_OS_ERROR;
|
||||
goto os_error;
|
||||
}
|
||||
|
||||
Py_END_ALLOW_THREADS
|
||||
|
||||
if (PyErr_CheckSignals() < 0) {
|
||||
return -1;
|
||||
}
|
||||
} while (now < deadline_ts);
|
||||
} while (now_ts < deadline_ts);
|
||||
|
||||
# undef RETURN_OS_ERROR
|
||||
|
||||
return -2;
|
||||
return US_ERROR_NO_DATA;
|
||||
}
|
||||
|
||||
static PyObject *_MemsinkObject_wait_frame(_MemsinkObject *self, PyObject *args, PyObject *kwargs) {
|
||||
@@ -195,17 +221,17 @@ static PyObject *_MemsinkObject_wait_frame(_MemsinkObject *self, PyObject *args,
|
||||
|
||||
switch (_wait_frame(self)) {
|
||||
case 0: break;
|
||||
case -2: Py_RETURN_NONE;
|
||||
case US_ERROR_NO_DATA: Py_RETURN_NONE;
|
||||
default: return NULL;
|
||||
}
|
||||
|
||||
us_frame_set_data(self->frame, _MEM(data), _MEM(used));
|
||||
us_memsink_shared_s *mem = self->mem;
|
||||
us_frame_set_data(self->frame, us_memsinksh_get_data(mem), mem->used);
|
||||
US_FRAME_COPY_META(self->mem, self->frame);
|
||||
self->frame_id = _MEM(id);
|
||||
self->frame_id = mem->id;
|
||||
self->frame_ts = us_get_now_monotonic();
|
||||
_MEM(last_client_ts) = self->frame_ts;
|
||||
if (key_required) {
|
||||
_MEM(key_requested) = true;
|
||||
mem->wants.key = true;
|
||||
}
|
||||
|
||||
if (flock(self->fd, LOCK_UN) < 0) {
|
||||
@@ -217,18 +243,19 @@ static PyObject *_MemsinkObject_wait_frame(_MemsinkObject *self, PyObject *args,
|
||||
return NULL;
|
||||
}
|
||||
|
||||
# define SET_VALUE(_key, _maker) { \
|
||||
PyObject *_tmp = _maker; \
|
||||
if (_tmp == NULL) { \
|
||||
# define SET_VALUE(x_key, x_maker) { \
|
||||
PyObject *m_tmp = x_maker; \
|
||||
if (m_tmp == NULL) { \
|
||||
return NULL; \
|
||||
} \
|
||||
if (PyDict_SetItemString(dict_frame, _key, _tmp) < 0) { \
|
||||
Py_DECREF(_tmp); \
|
||||
if (PyDict_SetItemString(dict_frame, x_key, m_tmp) < 0) { \
|
||||
Py_DECREF(m_tmp); \
|
||||
return NULL; \
|
||||
} \
|
||||
Py_DECREF(_tmp); \
|
||||
Py_DECREF(m_tmp); \
|
||||
}
|
||||
# define SET_NUMBER(_key, _from, _to) SET_VALUE(#_key, Py##_to##_From##_from(_FRAME(_key)))
|
||||
# define SET_NUMBER(x_key, x_from, x_to) \
|
||||
SET_VALUE(#x_key, Py##x_to##_From##x_from(self->frame->x_key))
|
||||
|
||||
SET_NUMBER(width, Long, Long);
|
||||
SET_NUMBER(height, Long, Long);
|
||||
@@ -237,10 +264,11 @@ static PyObject *_MemsinkObject_wait_frame(_MemsinkObject *self, PyObject *args,
|
||||
SET_NUMBER(online, Long, Bool);
|
||||
SET_NUMBER(key, Long, Bool);
|
||||
SET_NUMBER(gop, Long, Long);
|
||||
SET_NUMBER(grab_ts, Double, Float);
|
||||
SET_NUMBER(grab_begin_ts, Double, Float);
|
||||
SET_NUMBER(grab_end_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)));
|
||||
SET_VALUE("data", PyBytes_FromStringAndSize((const char*)self->frame->data, self->frame->used));
|
||||
|
||||
# undef SET_NUMBER
|
||||
# undef SET_VALUE
|
||||
@@ -252,21 +280,19 @@ static PyObject *_MemsinkObject_is_opened(_MemsinkObject *self, PyObject *Py_UNU
|
||||
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); \
|
||||
#define FIELD_GETTER(x_field, x_from, x_to) \
|
||||
static PyObject *_MemsinkObject_getter_##x_field(_MemsinkObject *self, void *Py_UNUSED(closure)) { \
|
||||
return Py##x_to##_From##x_from(self->x_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)}
|
||||
# define ADD_METHOD(x_name, x_method, x_flags) \
|
||||
{.ml_name = x_name, .ml_meth = (PyCFunction)_MemsinkObject_##x_method, .ml_flags = (x_flags)}
|
||||
ADD_METHOD("close", close, METH_NOARGS),
|
||||
ADD_METHOD("__enter__", enter, METH_NOARGS),
|
||||
ADD_METHOD("__exit__", exit, METH_VARARGS),
|
||||
@@ -277,7 +303,8 @@ static PyMethodDef _MemsinkObject_methods[] = {
|
||||
};
|
||||
|
||||
static PyGetSetDef _MemsinkObject_getsets[] = {
|
||||
# define ADD_GETTER(_field) {.name = #_field, .get = (getter)_MemsinkObject_getter_##_field}
|
||||
# define ADD_GETTER(x_field) \
|
||||
{.name = #x_field, .get = (getter)_MemsinkObject_getter_##x_field}
|
||||
ADD_GETTER(obj),
|
||||
ADD_GETTER(lock_timeout),
|
||||
ADD_GETTER(wait_timeout),
|
||||
@@ -305,21 +332,31 @@ static PyModuleDef _Module = {
|
||||
.m_size = -1,
|
||||
};
|
||||
|
||||
PyMODINIT_FUNC PyInit_ustreamer(void) { // cppcheck-suppress unusedFunction
|
||||
PyObject *module = PyModule_Create(&_Module);
|
||||
if (module == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
PyMODINIT_FUNC PyInit_ustreamer(void) {
|
||||
PyObject *module = NULL;
|
||||
|
||||
if (PyType_Ready(&_MemsinkType) < 0) {
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
|
||||
Py_INCREF(&_MemsinkType);
|
||||
|
||||
if (PyModule_AddObject(module, "Memsink", (PyObject *)&_MemsinkType) < 0) {
|
||||
return NULL;
|
||||
if ((module = PyModule_Create(&_Module)) == NULL) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
# define ADD(x_what, x_key, x_value) \
|
||||
{ if (PyModule_Add##x_what(module, x_key, x_value) < 0) { goto error; } }
|
||||
ADD(StringConstant, "__version__", US_VERSION);
|
||||
ADD(StringConstant, "VERSION", US_VERSION);
|
||||
ADD(IntConstant, "VERSION_MAJOR", US_VERSION_MAJOR);
|
||||
ADD(IntConstant, "VERSION_MINOR", US_VERSION_MINOR);
|
||||
ADD(StringConstant, "FEATURES", US_FEATURES); // Defined in setup.py
|
||||
ADD(ObjectRef, "Memsink", (PyObject*)&_MemsinkType);
|
||||
# undef ADD
|
||||
return module;
|
||||
|
||||
error:
|
||||
if (module != NULL) {
|
||||
Py_DECREF(module);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
114
src/Makefile
114
src/Makefile
@@ -1,7 +1,8 @@
|
||||
DESTDIR ?=
|
||||
R_DESTDIR ?=
|
||||
PREFIX ?= /usr/local
|
||||
|
||||
CC ?= gcc
|
||||
PKG_CONFIG ?= pkg-config
|
||||
CFLAGS ?= -O3
|
||||
LDFLAGS ?=
|
||||
|
||||
@@ -9,13 +10,14 @@ LDFLAGS ?=
|
||||
# =====
|
||||
_USTR = ustreamer.bin
|
||||
_DUMP = ustreamer-dump.bin
|
||||
_V4P = ustreamer-v4p.bin
|
||||
|
||||
_CFLAGS = -MD -c -std=c17 -Wall -Wextra -D_GNU_SOURCE $(CFLAGS)
|
||||
_LDFLAGS = $(LDFLAGS)
|
||||
|
||||
_COMMON_LIBS = -lm -ljpeg -pthread -lrt
|
||||
_USTR_LDFLAGS = $(LDFLAGS) -lm -ljpeg -pthread -lrt -levent -levent_pthreads
|
||||
_DUMP_LDFLAGS = $(LDFLAGS) -lm -ljpeg -pthread -lrt
|
||||
_V4P_LDFLAGS = $(LDFLAGS) -lm -ljpeg -pthread -lrt
|
||||
|
||||
_USTR_LIBS = $(_COMMON_LIBS) -levent -levent_pthreads
|
||||
_USTR_SRCS = $(shell ls \
|
||||
libs/*.c \
|
||||
ustreamer/*.c \
|
||||
@@ -26,83 +28,115 @@ _USTR_SRCS = $(shell ls \
|
||||
ustreamer/*.c \
|
||||
)
|
||||
|
||||
_DUMP_LIBS = $(_COMMON_LIBS)
|
||||
_DUMP_SRCS = $(shell ls \
|
||||
libs/*.c \
|
||||
dump/*.c \
|
||||
)
|
||||
|
||||
_V4P_SRCS = $(shell ls \
|
||||
libs/*.c \
|
||||
libs/drm/*.c \
|
||||
v4p/*.c \
|
||||
)
|
||||
|
||||
_BUILD = build
|
||||
|
||||
|
||||
define optbool
|
||||
$(filter $(shell echo $(1) | tr A-Z a-z), yes on 1)
|
||||
endef
|
||||
_TARGETS = $(_USTR) $(_DUMP)
|
||||
_OBJS = $(_USTR_SRCS:%.c=$(_BUILD)/%.o) $(_DUMP_SRCS:%.c=$(_BUILD)/%.o)
|
||||
|
||||
|
||||
ifneq ($(call optbool,$(WITH_GPIO)),)
|
||||
_USTR_LIBS += -lgpiod
|
||||
override _CFLAGS += -DWITH_GPIO
|
||||
_USTR_SRCS += $(shell ls ustreamer/gpio/*.c)
|
||||
# =====
|
||||
ifeq ($(findstring bsd,$(shell $(CC) -dumpmachine)),)
|
||||
override _USTR_LDFLAGS += -latomic
|
||||
override _DUMP_LDFLAGS += -latomic
|
||||
override _V4P_LDFLAGS += -latomic
|
||||
endif
|
||||
|
||||
|
||||
ifneq ($(call optbool,$(WITH_SYSTEMD)),)
|
||||
_USTR_LIBS += -lsystemd
|
||||
override _CFLAGS += -DWITH_SYSTEMD
|
||||
_USTR_SRCS += $(shell ls ustreamer/http/systemd/*.c)
|
||||
ifneq ($(MK_WITH_PYTHON),)
|
||||
override _CFLAGS += -DMK_WITH_PYTHON
|
||||
endif
|
||||
|
||||
|
||||
WITH_PTHREAD_NP ?= 1
|
||||
ifneq ($(call optbool,$(WITH_PTHREAD_NP)),)
|
||||
override _CFLAGS += -DWITH_PTHREAD_NP
|
||||
ifneq ($(MK_WITH_JANUS),)
|
||||
override _CFLAGS += -DMK_WITH_JANUS
|
||||
endif
|
||||
|
||||
|
||||
WITH_SETPROCTITLE ?= 1
|
||||
ifneq ($(call optbool,$(WITH_SETPROCTITLE)),)
|
||||
ifeq ($(shell uname -s | tr A-Z a-z),linux)
|
||||
_USTR_LIBS += -lbsd
|
||||
ifneq ($(MK_WITH_GPIO),)
|
||||
override _CFLAGS += -DMK_WITH_GPIO -DWITH_GPIO $(shell $(PKG_CONFIG) --atleast-version=2 libgpiod 2> /dev/null && echo -DHAVE_GPIOD2)
|
||||
override _USTR_LDFLAGS += -lgpiod
|
||||
override _USTR_SRCS += $(shell ls ustreamer/gpio/*.c)
|
||||
endif
|
||||
override _CFLAGS += -DWITH_SETPROCTITLE
|
||||
|
||||
ifneq ($(MK_WITH_SYSTEMD),)
|
||||
override _CFLAGS += -DMK_WITH_SYSTEMD -DWITH_SYSTEMD
|
||||
override _USTR_LDFLAGS += -lsystemd
|
||||
override _USTR_SRCS += $(shell ls ustreamer/http/systemd/*.c)
|
||||
endif
|
||||
|
||||
ifneq ($(MK_WITH_PTHREAD_NP),)
|
||||
override _CFLAGS += -DMK_WITH_PTHREAD_NP -DWITH_PTHREAD_NP
|
||||
endif
|
||||
|
||||
ifneq ($(MK_WITH_SETPROCTITLE),)
|
||||
override _CFLAGS += -DMK_WITH_SETPROCTITLE -DWITH_SETPROCTITLE
|
||||
ifneq ($(findstring linux,$(shell $(CC) -dumpmachine)),)
|
||||
override _USTR_LDFLAGS += -lbsd
|
||||
endif
|
||||
endif
|
||||
|
||||
ifneq ($(MK_WITH_PDEATHSIG),)
|
||||
override _CFLAGS += -DMK_WITH_PDEATHSIG -DWITH_PDEATHSIG
|
||||
endif
|
||||
|
||||
ifneq ($(MK_WITH_V4P),)
|
||||
override _TARGETS += $(_V4P)
|
||||
override _OBJS += $(_V4P_SRCS:%.c=$(_BUILD)/%.o)
|
||||
override _CFLAGS += -DMK_WITH_V4P -DWITH_V4P $(shell $(PKG_CONFIG) --cflags libdrm)
|
||||
override _V4P_LDFLAGS += $(shell $(PKG_CONFIG) --libs libdrm)
|
||||
override _USTR_SRCS += $(shell ls libs/drm/*.c)
|
||||
override _USTR_LDFLAGS += $(shell $(PKG_CONFIG) --libs libdrm)
|
||||
endif
|
||||
|
||||
|
||||
# =====
|
||||
all: $(_USTR) $(_DUMP)
|
||||
all: $(_TARGETS)
|
||||
|
||||
|
||||
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))
|
||||
mkdir -p $(R_DESTDIR)$(PREFIX)/bin
|
||||
for i in $(subst .bin,,$(_TARGETS)); do \
|
||||
install -m755 $$i.bin $(R_DESTDIR)$(PREFIX)/bin/$$i; \
|
||||
done
|
||||
|
||||
|
||||
install-strip: install
|
||||
strip $(DESTDIR)$(PREFIX)/bin/$(subst .bin,,$(_USTR))
|
||||
strip $(DESTDIR)$(PREFIX)/bin/$(subst .bin,,$(_DUMP))
|
||||
for i in $(subst .bin,,$(_TARGETS)); do \
|
||||
strip $(R_DESTDIR)$(PREFIX)/bin/$$i; \
|
||||
done
|
||||
|
||||
|
||||
$(_USTR): $(_USTR_SRCS:%.c=$(_BUILD)/%.o)
|
||||
$(info == LD $@)
|
||||
@ $(CC) $^ -o $@ $(_LDFLAGS) $(_USTR_LIBS)
|
||||
$(ECHO) $(CC) $^ -o $@ $(_USTR_LDFLAGS)
|
||||
|
||||
|
||||
$(_DUMP): $(_DUMP_SRCS:%.c=$(_BUILD)/%.o)
|
||||
$(info == LD $@)
|
||||
@ $(CC) $^ -o $@ $(_LDFLAGS) $(_DUMP_LIBS)
|
||||
$(ECHO) $(CC) $^ -o $@ $(_DUMP_LDFLAGS)
|
||||
|
||||
|
||||
$(_V4P): $(_V4P_SRCS:%.c=$(_BUILD)/%.o)
|
||||
$(info == LD $@)
|
||||
$(ECHO) $(CC) $^ -o $@ $(_V4P_LDFLAGS)
|
||||
|
||||
|
||||
$(_BUILD)/%.o: %.c
|
||||
$(info -- CC $<)
|
||||
@ mkdir -p $(dir $@) || true
|
||||
@ $(CC) $< -o $@ $(_CFLAGS)
|
||||
$(ECHO) mkdir -p $(dir $@) || true
|
||||
$(ECHO) $(CC) $< -o $@ $(_CFLAGS)
|
||||
|
||||
|
||||
clean:
|
||||
rm -rf $(_USTR) $(_DUMP) $(_BUILD)
|
||||
rm -rf $(_USTR) $(_DUMP) $(_V4P) $(_BUILD)
|
||||
|
||||
|
||||
_OBJS = $(_USTR_SRCS:%.c=$(_BUILD)/%.o) $(_DUMP_SRCS:%.c=$(_BUILD)/%.o)
|
||||
-include $(_OBJS:%.o=%.d)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -24,54 +24,73 @@
|
||||
|
||||
|
||||
us_output_file_s *us_output_file_init(const char *path, bool json) {
|
||||
us_output_file_s *output;
|
||||
US_CALLOC(output, 1);
|
||||
us_output_file_s *out;
|
||||
US_CALLOC(out, 1);
|
||||
|
||||
if (!strcmp(path, "-")) {
|
||||
US_LOG_INFO("Using output: <stdout>");
|
||||
output->fp = stdout;
|
||||
out->fp = stdout;
|
||||
} else {
|
||||
US_LOG_INFO("Using output: %s", path);
|
||||
if ((output->fp = fopen(path, "wb")) == NULL) {
|
||||
if ((out->fp = fopen(path, "wb")) == NULL) {
|
||||
US_LOG_PERROR("Can't open output file");
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
output->json = json;
|
||||
return output;
|
||||
out->json = json;
|
||||
return out;
|
||||
|
||||
error:
|
||||
us_output_file_destroy(output);
|
||||
us_output_file_destroy(out);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void us_output_file_write(void *v_output, const us_frame_s *frame) {
|
||||
us_output_file_s *output = (us_output_file_s *)v_output;
|
||||
if (output->json) {
|
||||
us_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, \"key\": %u, \"gop\": %u,"
|
||||
" \"grab_ts\": %.3Lf, \"encode_begin_ts\": %.3Lf, \"encode_end_ts\": %.3Lf,"
|
||||
void us_output_file_write(void *v_out, const us_frame_s *frame) {
|
||||
us_output_file_s *out = v_out;
|
||||
if (out->json) {
|
||||
us_base64_encode(frame->data, frame->used, &out->base64_data, &out->base64_allocated);
|
||||
fprintf(
|
||||
out->fp,
|
||||
"{\"size\": %zu,"
|
||||
" \"width\": %u,"
|
||||
" \"height\": %u,"
|
||||
" \"format\": %u,"
|
||||
" \"stride\": %u,"
|
||||
" \"online\": %u,"
|
||||
" \"key\": %u,"
|
||||
" \"gop\": %u,"
|
||||
" \"grab_begin_ts\": %.3Lf,"
|
||||
" \"grab_end_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->key, frame->gop,
|
||||
frame->grab_ts, frame->encode_begin_ts, frame->encode_end_ts,
|
||||
output->base64_data);
|
||||
frame->used,
|
||||
frame->width,
|
||||
frame->height,
|
||||
frame->format,
|
||||
frame->stride,
|
||||
frame->online,
|
||||
frame->key,
|
||||
frame->gop,
|
||||
frame->grab_begin_ts,
|
||||
frame->grab_end_ts,
|
||||
frame->encode_begin_ts,
|
||||
frame->encode_end_ts,
|
||||
out->base64_data);
|
||||
} else {
|
||||
fwrite(frame->data, 1, frame->used, output->fp);
|
||||
fwrite(frame->data, 1, frame->used, out->fp);
|
||||
}
|
||||
fflush(output->fp);
|
||||
fflush(out->fp);
|
||||
}
|
||||
|
||||
void us_output_file_destroy(void *v_output) {
|
||||
us_output_file_s *output = (us_output_file_s *)v_output;
|
||||
US_DELETE(output->base64_data, free);
|
||||
if (output->fp && output->fp != stdout) {
|
||||
if (fclose(output->fp) < 0) {
|
||||
void us_output_file_destroy(void *v_out) {
|
||||
us_output_file_s *out = v_out;
|
||||
US_DELETE(out->base64_data, free);
|
||||
if (out->fp && out->fp != stdout) {
|
||||
if (fclose(out->fp) < 0) {
|
||||
US_LOG_PERROR("Can't close output file");
|
||||
}
|
||||
}
|
||||
free(output);
|
||||
free(out);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -45,5 +45,5 @@ typedef struct {
|
||||
|
||||
|
||||
us_output_file_s *us_output_file_init(const char *path, bool json);
|
||||
void us_output_file_write(void *v_output, const us_frame_s *frame);
|
||||
void us_output_file_destroy(void *v_output);
|
||||
void us_output_file_write(void *v_out, const us_frame_s *frame);
|
||||
void us_output_file_destroy(void *v_out);
|
||||
|
||||
141
src/dump/main.c
141
src/dump/main.c
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -24,18 +24,19 @@
|
||||
#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>
|
||||
|
||||
#include "../libs/const.h"
|
||||
#include "../libs/errors.h"
|
||||
#include "../libs/tools.h"
|
||||
#include "../libs/logging.h"
|
||||
#include "../libs/frame.h"
|
||||
#include "../libs/memsink.h"
|
||||
#include "../libs/fpsi.h"
|
||||
#include "../libs/signal.h"
|
||||
#include "../libs/options.h"
|
||||
|
||||
#include "file.h"
|
||||
@@ -88,18 +89,19 @@ volatile bool _g_stop = false;
|
||||
|
||||
|
||||
typedef struct {
|
||||
void *v_output;
|
||||
void (*write)(void *v_output, const us_frame_s *frame);
|
||||
void (*destroy)(void *v_output);
|
||||
void *v_out;
|
||||
void (*write)(void *v_out, const us_frame_s *frame);
|
||||
void (*destroy)(void *v_out);
|
||||
} _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,
|
||||
long long count, long double interval,
|
||||
const char *sink_name,
|
||||
unsigned sink_timeout,
|
||||
long long count,
|
||||
long double interval,
|
||||
bool key_required,
|
||||
_output_context_s *ctx);
|
||||
|
||||
@@ -110,10 +112,10 @@ int main(int argc, char *argv[]) {
|
||||
US_LOGGING_INIT;
|
||||
US_THREAD_RENAME("main");
|
||||
|
||||
char *sink_name = NULL;
|
||||
const char *sink_name = NULL;
|
||||
unsigned sink_timeout = 1;
|
||||
char *output_path = NULL;
|
||||
bool output_json = false;
|
||||
const char *out_path = NULL;
|
||||
bool out_json = false;
|
||||
long long count = 0;
|
||||
long double interval = 0;
|
||||
bool key_required = false;
|
||||
@@ -150,8 +152,8 @@ int main(int argc, char *argv[]) {
|
||||
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_OUTPUT: OPT_SET(out_path, optarg);
|
||||
case _O_OUTPUT_JSON: OPT_SET(out_json, true);
|
||||
case _O_COUNT: OPT_NUMBER("--count", count, 0, LLONG_MAX, 0);
|
||||
case _O_INTERVAL: OPT_LDOUBLE("--interval", interval, 0, 60);
|
||||
case _O_KEY_REQUIRED: OPT_SET(key_required, true);
|
||||
@@ -175,25 +177,25 @@ int main(int argc, char *argv[]) {
|
||||
# undef OPT_NUMBER
|
||||
# undef OPT_SET
|
||||
|
||||
if (sink_name == NULL || sink_name[0] == '\0') {
|
||||
if (!us_str_is_ok(sink_name)) {
|
||||
puts("Missing option --sink. See --help for details.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
_output_context_s ctx = {0};
|
||||
|
||||
if (output_path && output_path[0] != '\0') {
|
||||
if ((ctx.v_output = (void *)us_output_file_init(output_path, output_json)) == NULL) {
|
||||
if (us_str_is_ok(out_path)) {
|
||||
if ((ctx.v_out = (void*)us_output_file_init(out_path, out_json)) == NULL) {
|
||||
return 1;
|
||||
}
|
||||
ctx.write = us_output_file_write;
|
||||
ctx.destroy = us_output_file_destroy;
|
||||
}
|
||||
|
||||
_install_signal_handlers();
|
||||
us_install_signals_handler(_signal_handler, false);
|
||||
const int retval = abs(_dump_sink(sink_name, sink_timeout, count, interval, key_required, &ctx));
|
||||
if (ctx.v_output && ctx.destroy) {
|
||||
ctx.destroy(ctx.v_output);
|
||||
if (ctx.v_out && ctx.destroy) {
|
||||
ctx.destroy(ctx.v_out);
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
@@ -206,30 +208,16 @@ static void _signal_handler(int signum) {
|
||||
_g_stop = true;
|
||||
}
|
||||
|
||||
static void _install_signal_handlers(void) {
|
||||
struct sigaction sig_act = {0};
|
||||
|
||||
assert(!sigemptyset(&sig_act.sa_mask));
|
||||
sig_act.sa_handler = _signal_handler;
|
||||
assert(!sigaddset(&sig_act.sa_mask, SIGINT));
|
||||
assert(!sigaddset(&sig_act.sa_mask, SIGTERM));
|
||||
assert(!sigaddset(&sig_act.sa_mask, SIGPIPE));
|
||||
|
||||
US_LOG_DEBUG("Installing SIGINT handler ...");
|
||||
assert(!sigaction(SIGINT, &sig_act, NULL));
|
||||
|
||||
US_LOG_DEBUG("Installing SIGTERM handler ...");
|
||||
assert(!sigaction(SIGTERM, &sig_act, NULL));
|
||||
|
||||
US_LOG_DEBUG("Installing SIGTERM handler ...");
|
||||
assert(!sigaction(SIGPIPE, &sig_act, NULL));
|
||||
}
|
||||
|
||||
static int _dump_sink(
|
||||
const char *sink_name, unsigned sink_timeout,
|
||||
long long count, long double interval,
|
||||
const char *sink_name,
|
||||
unsigned sink_timeout,
|
||||
long long count,
|
||||
long double interval,
|
||||
bool key_required,
|
||||
_output_context_s *ctx) {
|
||||
_output_context_s *ctx
|
||||
) {
|
||||
|
||||
int retval = -1;
|
||||
|
||||
if (count == 0) {
|
||||
count = -1;
|
||||
@@ -238,49 +226,46 @@ static int _dump_sink(
|
||||
const useconds_t interval_us = interval * 1000000;
|
||||
|
||||
us_frame_s *frame = us_frame_init();
|
||||
us_fpsi_s *fpsi = us_fpsi_init("SINK", false);
|
||||
us_memsink_s *sink = NULL;
|
||||
|
||||
if ((sink = us_memsink_init("input", sink_name, false, 0, false, 0, sink_timeout)) == NULL) {
|
||||
if ((sink = us_memsink_init_opened("input", sink_name, false, 0, false, 0, sink_timeout)) == NULL) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
unsigned fps = 0;
|
||||
unsigned fps_accum = 0;
|
||||
long long fps_second = 0;
|
||||
|
||||
long double last_ts = 0;
|
||||
|
||||
while (!_g_stop) {
|
||||
bool key_requested;
|
||||
const int error = us_memsink_client_get(sink, frame, &key_requested, key_required);
|
||||
if (error == 0) {
|
||||
us_memsink_wants_s w_get = {0};
|
||||
us_memsink_wants_s w_put = {.key = key_required};
|
||||
const int got = us_memsink_client_get(sink, frame, &w_get, (key_required ? &w_put : NULL));
|
||||
if (got == 0) {
|
||||
key_required = false;
|
||||
|
||||
const long double now = us_get_now_monotonic();
|
||||
const long long now_second = us_floor_ms(now);
|
||||
|
||||
char fourcc_str[8];
|
||||
US_LOG_VERBOSE("Frame: %s - %ux%u -- online=%d, key=%d, kr=%d, gop=%u, latency=%.3Lf, backlog=%.3Lf, size=%zu",
|
||||
US_LOG_VERBOSE("%s %.3Lf - %s %ux%u - gop=%u, key=%u, kr=%u - GRAB=%.3Lf ~~%.3Lf~~ ENC=%.3Lf ~~> LAT=%.3Lf - size=%zu",
|
||||
(frame->online ? " ON" : "OFF"),
|
||||
(last_ts ? now - last_ts : 0),
|
||||
us_fourcc_to_string(frame->format, fourcc_str, 8),
|
||||
frame->width, frame->height,
|
||||
frame->online, frame->key, key_requested, frame->gop,
|
||||
now - frame->grab_ts, (last_ts ? now - last_ts : 0),
|
||||
frame->width,
|
||||
frame->height,
|
||||
frame->gop,
|
||||
frame->key,
|
||||
w_get.key,
|
||||
frame->grab_end_ts - frame->grab_begin_ts,
|
||||
frame->encode_begin_ts - frame->grab_end_ts,
|
||||
frame->encode_end_ts - frame->encode_begin_ts,
|
||||
now - frame->grab_begin_ts,
|
||||
frame->used);
|
||||
|
||||
last_ts = now;
|
||||
|
||||
US_LOG_DEBUG(" stride=%u, grab_ts=%.3Lf, encode_begin_ts=%.3Lf, encode_end_ts=%.3Lf",
|
||||
frame->stride, frame->grab_ts, frame->encode_begin_ts, frame->encode_end_ts);
|
||||
us_fpsi_update(fpsi, true, NULL);
|
||||
|
||||
if (now_second != fps_second) {
|
||||
fps = fps_accum;
|
||||
fps_accum = 0;
|
||||
fps_second = now_second;
|
||||
US_LOG_PERF_FPS("A new second has come; captured_fps=%u", fps);
|
||||
}
|
||||
fps_accum += 1;
|
||||
|
||||
if (ctx->v_output != NULL) {
|
||||
ctx->write(ctx->v_output, frame);
|
||||
if (ctx->v_out != NULL) {
|
||||
ctx->write(ctx->v_out, frame);
|
||||
}
|
||||
|
||||
if (count >= 0) {
|
||||
@@ -293,25 +278,21 @@ static int _dump_sink(
|
||||
if (interval_us > 0) {
|
||||
usleep(interval_us);
|
||||
}
|
||||
} else if (error == -2) {
|
||||
} else if (got == US_ERROR_NO_DATA) {
|
||||
usleep(1000);
|
||||
} else {
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
int retval = 0;
|
||||
goto ok;
|
||||
retval = 0;
|
||||
|
||||
error:
|
||||
retval = -1;
|
||||
|
||||
ok:
|
||||
US_DELETE(sink, us_memsink_destroy);
|
||||
us_frame_destroy(frame);
|
||||
|
||||
US_LOG_INFO("Bye-bye");
|
||||
return retval;
|
||||
error:
|
||||
US_DELETE(sink, us_memsink_destroy);
|
||||
us_fpsi_destroy(fpsi);
|
||||
us_frame_destroy(frame);
|
||||
US_LOG_INFO("Bye-bye");
|
||||
return retval;
|
||||
}
|
||||
|
||||
static void _help(FILE *fp) {
|
||||
@@ -319,7 +300,7 @@ static void _help(FILE *fp) {
|
||||
SAY("\nuStreamer-dump - Dump uStreamer's memory sink to file");
|
||||
SAY("═════════════════════════════════════════════════════");
|
||||
SAY("Version: %s; license: GPLv3", US_VERSION);
|
||||
SAY("Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com>\n");
|
||||
SAY("Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com>\n");
|
||||
SAY("Example:");
|
||||
SAY("════════");
|
||||
SAY(" ustreamer-dump --sink test --output - \\");
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,16 +22,16 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <assert.h>
|
||||
#include "tools.h"
|
||||
|
||||
|
||||
#define US_ARRAY_LEN(x_array) (sizeof(x_array) / sizeof((x_array)[0]))
|
||||
|
||||
#define US_ARRAY_ITERATE(x_array, x_start, x_item_ptr, ...) { \
|
||||
const int m_len = US_ARRAY_LEN(x_array); \
|
||||
assert(x_start <= m_len); \
|
||||
for (int m_index = x_start; m_index < m_len; ++m_index) { \
|
||||
__typeof__((x_array)[0]) *const x_item_ptr = &x_array[m_index]; \
|
||||
US_A(x_start <= m_len); \
|
||||
for (int m_i = x_start; m_i < m_len; ++m_i) { \
|
||||
__typeof__((x_array)[0]) *const x_item_ptr = &x_array[m_i]; \
|
||||
__VA_ARGS__ \
|
||||
} \
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,6 +22,13 @@
|
||||
|
||||
#include "base64.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "types.h"
|
||||
#include "tools.h"
|
||||
|
||||
|
||||
static const char _ENCODING_TABLE[] = {
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
|
||||
@@ -34,11 +41,11 @@ static const char _ENCODING_TABLE[] = {
|
||||
'4', '5', '6', '7', '8', '9', '+', '/',
|
||||
};
|
||||
|
||||
static const unsigned _MOD_TABLE[] = {0, 2, 1};
|
||||
static const uint _MOD_TABLE[] = {0, 2, 1};
|
||||
|
||||
|
||||
void us_base64_encode(const uint8_t *data, size_t size, char **encoded, size_t *allocated) {
|
||||
const size_t encoded_size = 4 * ((size + 2) / 3) + 1; // +1 for '\0'
|
||||
void us_base64_encode(const u8 *data, uz size, char **encoded, uz *allocated) {
|
||||
const uz encoded_size = 4 * ((size + 2) / 3) + 1; // +1 for '\0'
|
||||
|
||||
if (*encoded == NULL || (allocated && *allocated < encoded_size)) {
|
||||
US_REALLOC(*encoded, encoded_size);
|
||||
@@ -47,16 +54,16 @@ void us_base64_encode(const uint8_t *data, size_t size, char **encoded, size_t *
|
||||
}
|
||||
}
|
||||
|
||||
for (unsigned data_index = 0, encoded_index = 0; data_index < size;) {
|
||||
# define OCTET(_name) unsigned _name = (data_index < size ? (uint8_t)data[data_index++] : 0)
|
||||
for (uint data_i = 0, encoded_i = 0; data_i < size;) {
|
||||
# define OCTET(_name) uint _name = (data_i < size ? (u8)data[data_i++] : 0)
|
||||
OCTET(octet_a);
|
||||
OCTET(octet_b);
|
||||
OCTET(octet_c);
|
||||
# undef OCTET
|
||||
|
||||
const unsigned triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c;
|
||||
const uint triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c;
|
||||
|
||||
# define ENCODE(_offset) (*encoded)[encoded_index++] = _ENCODING_TABLE[(triple >> _offset * 6) & 0x3F]
|
||||
# define ENCODE(_offset) (*encoded)[encoded_i++] = _ENCODING_TABLE[(triple >> _offset * 6) & 0x3F]
|
||||
ENCODE(3);
|
||||
ENCODE(2);
|
||||
ENCODE(1);
|
||||
@@ -64,7 +71,7 @@ void us_base64_encode(const uint8_t *data, size_t size, char **encoded, size_t *
|
||||
# undef ENCODE
|
||||
}
|
||||
|
||||
for (unsigned index = 0; index < _MOD_TABLE[size % 3]; index++) {
|
||||
for (uint index = 0; index < _MOD_TABLE[size % 3]; index++) {
|
||||
(*encoded)[encoded_size - 2 - index] = '=';
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,13 +22,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "tools.h"
|
||||
#include "types.h"
|
||||
|
||||
|
||||
void us_base64_encode(const uint8_t *data, size_t size, char **encoded, size_t *allocated);
|
||||
void us_base64_encode(const u8 *data, uz size, char **encoded, uz *allocated);
|
||||
|
||||
1384
src/libs/capture.c
Normal file
1384
src/libs/capture.c
Normal file
File diff suppressed because it is too large
Load Diff
116
src/libs/capture.h
Normal file
116
src/libs/capture.h
Normal file
@@ -0,0 +1,116 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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 <stdatomic.h>
|
||||
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#include "types.h"
|
||||
#include "frame.h"
|
||||
#include "media.h"
|
||||
#include "controls.h"
|
||||
|
||||
|
||||
#define US_VIDEO_MIN_WIDTH ((uint)160)
|
||||
#define US_VIDEO_MAX_WIDTH ((uint)15360) // Remember about stream->run->http_capture_state;
|
||||
|
||||
#define US_VIDEO_MIN_HEIGHT ((uint)120)
|
||||
#define US_VIDEO_MAX_HEIGHT ((uint)8640)
|
||||
|
||||
#define US_VIDEO_MAX_FPS ((uint)120)
|
||||
|
||||
#define US_STANDARDS_STR "PAL, NTSC, SECAM"
|
||||
#define US_FORMATS_STR "YUYV, YVYU, UYVY, YUV420, YVU420, RGB565, RGB24, BGR24, GREY, MJPEG, JPEG"
|
||||
#define US_IO_METHODS_STR "MMAP, USERPTR"
|
||||
|
||||
|
||||
typedef struct {
|
||||
us_frame_s raw;
|
||||
struct v4l2_buffer buf;
|
||||
int dma_fd;
|
||||
bool grabbed;
|
||||
atomic_int refs;
|
||||
} us_capture_hwbuf_s;
|
||||
|
||||
typedef struct {
|
||||
int fd;
|
||||
int dv_timings_fd;
|
||||
us_media_pad_s media_pads[3]; // for VIDIOC_SUBDEV_S_FMT
|
||||
uint width;
|
||||
uint height;
|
||||
uint format;
|
||||
uint stride;
|
||||
float hz;
|
||||
uint jpeg_quality;
|
||||
uz raw_size;
|
||||
uint n_bufs;
|
||||
us_capture_hwbuf_s *bufs;
|
||||
bool dma;
|
||||
enum v4l2_buf_type capture_type;
|
||||
bool capture_mplane;
|
||||
bool streamon;
|
||||
int open_error_once;
|
||||
} us_capture_runtime_s;
|
||||
|
||||
typedef struct {
|
||||
char *path;
|
||||
uint input;
|
||||
uint width;
|
||||
uint height;
|
||||
uint format;
|
||||
char *media_path;
|
||||
char *media_entity_name;
|
||||
|
||||
bool format_swap_rgb;
|
||||
uint jpeg_quality;
|
||||
v4l2_std_id standard;
|
||||
enum v4l2_memory io_method;
|
||||
bool dv_timings;
|
||||
uint n_bufs;
|
||||
bool dma_export;
|
||||
bool dma_required;
|
||||
uz min_frame_size;
|
||||
bool allow_truncated_frames;
|
||||
bool persistent;
|
||||
uint timeout;
|
||||
us_controls_s *ctl;
|
||||
us_capture_runtime_s *run;
|
||||
} us_capture_s;
|
||||
|
||||
|
||||
us_capture_s *us_capture_init(void);
|
||||
void us_capture_destroy(us_capture_s *cap);
|
||||
|
||||
int us_capture_parse_format(const char *str);
|
||||
int us_capture_parse_standard(const char *str);
|
||||
int us_capture_parse_io_method(const char *str);
|
||||
|
||||
int us_capture_open(us_capture_s *cap);
|
||||
void us_capture_close(us_capture_s *cap);
|
||||
|
||||
int us_capture_hwbuf_grab(us_capture_s *cap, us_capture_hwbuf_s **hw);
|
||||
int us_capture_hwbuf_release(const us_capture_s *cap, us_capture_hwbuf_s *hw);
|
||||
|
||||
void us_capture_hwbuf_incref(us_capture_hwbuf_s *hw);
|
||||
void us_capture_hwbuf_decref(us_capture_hwbuf_s *hw);
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -20,45 +20,66 @@
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
#include "tc358743.h"
|
||||
#include "chip.h"
|
||||
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
#include <linux/videodev2.h>
|
||||
#include <linux/v4l2-controls.h>
|
||||
|
||||
#include "types.h"
|
||||
#include "errors.h"
|
||||
#include "tools.h"
|
||||
#include "xioctl.h"
|
||||
|
||||
|
||||
#ifndef V4L2_CID_USER_TC358743_BASE
|
||||
# define V4L2_CID_USER_TC358743_BASE (V4L2_CID_USER_BASE + 0x1080)
|
||||
#endif
|
||||
#ifndef TC358743_CID_AUDIO_PRESENT
|
||||
# define TC358743_CID_AUDIO_PRESENT (V4L2_CID_USER_TC358743_BASE + 1)
|
||||
#endif
|
||||
|
||||
#ifndef TC358743_CID_AUDIO_SAMPLING_RATE
|
||||
# define TC358743_CID_AUDIO_SAMPLING_RATE (V4L2_CID_USER_TC358743_BASE + 0)
|
||||
#endif
|
||||
|
||||
#ifndef TC358743_CID_AUDIO_PRESENT
|
||||
# define TC358743_CID_AUDIO_PRESENT (V4L2_CID_USER_TC358743_BASE + 1)
|
||||
#endif
|
||||
|
||||
int us_tc358743_read_info(const char *path, us_tc358743_info_s *info) {
|
||||
US_MEMSET_ZERO(*info);
|
||||
#ifndef TC358743_CID_LANES_ENOUGH
|
||||
# define TC358743_CID_LANES_ENOUGH (V4L2_CID_USER_TC358743_BASE + 2)
|
||||
#endif
|
||||
|
||||
int fd = -1;
|
||||
if ((fd = open(path, O_RDWR)) < 0) {
|
||||
US_JLOG_PERROR("audio", "Can't open TC358743 V4L2 device");
|
||||
return -1;
|
||||
|
||||
int us_chip_check_cable(int fd) {
|
||||
struct v4l2_control ctl = {.id = V4L2_CID_DV_RX_POWER_PRESENT};
|
||||
if (us_xioctl(fd, VIDIOC_G_CTRL, &ctl) < 0) {
|
||||
return US_ERROR_COMMON;
|
||||
}
|
||||
return (ctl.value ? 0 : US_ERROR_NO_CABLE);
|
||||
}
|
||||
|
||||
int us_chip_tc358743_check_lanes(int fd) {
|
||||
struct v4l2_control ctl = {.id = TC358743_CID_LANES_ENOUGH};
|
||||
if (us_xioctl(fd, VIDIOC_G_CTRL, &ctl) < 0) {
|
||||
return US_ERROR_COMMON;
|
||||
}
|
||||
return (ctl.value ? 0 : US_ERROR_NO_LANES);
|
||||
}
|
||||
|
||||
int us_chip_tc358743_get_audio_hz(int fd) {
|
||||
struct v4l2_control ctl = {.id = TC358743_CID_AUDIO_PRESENT};
|
||||
if (us_xioctl(fd, VIDIOC_G_CTRL, &ctl) < 0) {
|
||||
return US_ERROR_COMMON;
|
||||
}
|
||||
if (!ctl.value) {
|
||||
return US_ERROR_NO_SIGNAL; // No audio
|
||||
}
|
||||
|
||||
# define READ_CID(x_cid, x_field) { \
|
||||
struct v4l2_control m_ctl = {0}; \
|
||||
m_ctl.id = x_cid; \
|
||||
if (us_xioctl(fd, VIDIOC_G_CTRL, &m_ctl) < 0) { \
|
||||
US_JLOG_PERROR("audio", "Can't get value of " #x_cid); \
|
||||
close(fd); \
|
||||
return -1; \
|
||||
} \
|
||||
info->x_field = m_ctl.value; \
|
||||
}
|
||||
|
||||
READ_CID(TC358743_CID_AUDIO_PRESENT, has_audio);
|
||||
READ_CID(TC358743_CID_AUDIO_SAMPLING_RATE, audio_hz);
|
||||
|
||||
# undef READ_CID
|
||||
|
||||
close(fd);
|
||||
return 0;
|
||||
US_MEMSET_ZERO(ctl);
|
||||
ctl.id = TC358743_CID_AUDIO_SAMPLING_RATE;
|
||||
if (us_xioctl(fd, VIDIOC_G_CTRL, &ctl) < 0) {
|
||||
return US_ERROR_COMMON;
|
||||
}
|
||||
return (ctl.value ? ctl.value : US_ERROR_NO_SIGNAL);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,13 +22,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
#include "types.h"
|
||||
|
||||
|
||||
extern const unsigned US_BLANK_JPEG_WIDTH;
|
||||
extern const unsigned US_BLANK_JPEG_HEIGHT;
|
||||
int us_chip_check_cable(int fd);
|
||||
|
||||
extern const size_t US_BLANK_JPEG_DATA_SIZE;
|
||||
extern const uint8_t US_BLANK_JPEG_DATA[];
|
||||
int us_chip_tc358743_check_lanes(int fd);
|
||||
int us_chip_tc358743_get_audio_hz(int fd);
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,11 +22,14 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define US_VERSION_MAJOR 5
|
||||
#define US_VERSION_MINOR 46
|
||||
#include "types.h"
|
||||
|
||||
|
||||
#define US_VERSION_MAJOR 6
|
||||
#define US_VERSION_MINOR 64
|
||||
|
||||
#define US_MAKE_VERSION2(_major, _minor) #_major "." #_minor
|
||||
#define US_MAKE_VERSION1(_major, _minor) US_MAKE_VERSION2(_major, _minor)
|
||||
#define US_VERSION US_MAKE_VERSION1(US_VERSION_MAJOR, US_VERSION_MINOR)
|
||||
|
||||
#define US_VERSION_U ((unsigned)(US_VERSION_MAJOR * 1000 + US_VERSION_MINOR))
|
||||
#define US_VERSION_U ((uint)(US_VERSION_MAJOR * 1000 + US_VERSION_MINOR))
|
||||
|
||||
173
src/libs/controls.c
Normal file
173
src/libs/controls.c
Normal file
@@ -0,0 +1,173 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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 "controls.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include <linux/videodev2.h>
|
||||
#include <linux/v4l2-controls.h>
|
||||
|
||||
#include "types.h"
|
||||
#include "tools.h"
|
||||
#include "logging.h"
|
||||
#include "xioctl.h"
|
||||
|
||||
|
||||
static int _query_control(
|
||||
int fd,
|
||||
struct v4l2_queryctrl *query,
|
||||
const char *name,
|
||||
uint cid,
|
||||
bool quiet);
|
||||
|
||||
static void _set_control(
|
||||
int fd,
|
||||
const struct v4l2_queryctrl *query,
|
||||
const char *name,
|
||||
uint cid,
|
||||
int value,
|
||||
bool quiet);
|
||||
|
||||
|
||||
#define _LOG_ERROR(x_msg, ...) US_LOG_ERROR("CAP: " x_msg, ##__VA_ARGS__)
|
||||
#define _LOG_PERROR(x_msg, ...) US_LOG_PERROR("CAP: " x_msg, ##__VA_ARGS__)
|
||||
#define _LOG_INFO(x_msg, ...) US_LOG_INFO("CAP: " x_msg, ##__VA_ARGS__)
|
||||
|
||||
|
||||
us_controls_s *us_controls_init(void) {
|
||||
us_controls_s *ctl;
|
||||
US_CALLOC(ctl, 1);
|
||||
return ctl;
|
||||
}
|
||||
|
||||
void us_controls_destroy(us_controls_s *ctl) {
|
||||
free(ctl);
|
||||
}
|
||||
|
||||
void us_controls_apply(const us_controls_s *ctl, int fd) {
|
||||
# define SET_CID_VALUE(x_cid, x_field, x_value, x_quiet) { \
|
||||
struct v4l2_queryctrl m_query; \
|
||||
if (_query_control(fd, &m_query, #x_field, x_cid, x_quiet) == 0) { \
|
||||
_set_control(fd, &m_query, #x_field, x_cid, x_value, x_quiet); \
|
||||
} \
|
||||
}
|
||||
|
||||
# define SET_CID_DEFAULT(x_cid, x_field, x_quiet) { \
|
||||
struct v4l2_queryctrl m_query; \
|
||||
if (_query_control(fd, &m_query, #x_field, x_cid, x_quiet) == 0) { \
|
||||
_set_control(fd, &m_query, #x_field, x_cid, m_query.default_value, x_quiet); \
|
||||
} \
|
||||
}
|
||||
|
||||
# define CONTROL_MANUAL_CID(x_cid, x_field) { \
|
||||
if (ctl->x_field.mode == US_CTL_MODE_VALUE) { \
|
||||
SET_CID_VALUE(x_cid, x_field, ctl->x_field.value, false); \
|
||||
} else if (ctl->x_field.mode == US_CTL_MODE_DEFAULT) { \
|
||||
SET_CID_DEFAULT(x_cid, x_field, false); \
|
||||
} \
|
||||
}
|
||||
|
||||
# define CONTROL_AUTO_CID(x_cid_auto, x_cid_manual, x_field) { \
|
||||
if (ctl->x_field.mode == US_CTL_MODE_VALUE) { \
|
||||
SET_CID_VALUE(x_cid_auto, x_field##_auto, 0, true); \
|
||||
SET_CID_VALUE(x_cid_manual, x_field, ctl->x_field.value, false); \
|
||||
} else if (ctl->x_field.mode == US_CTL_MODE_AUTO) { \
|
||||
SET_CID_VALUE(x_cid_auto, x_field##_auto, 1, false); \
|
||||
} else if (ctl->x_field.mode == US_CTL_MODE_DEFAULT) { \
|
||||
SET_CID_VALUE(x_cid_auto, x_field##_auto, 0, true); /* Reset inactive flag */ \
|
||||
SET_CID_DEFAULT(x_cid_manual, x_field, false); \
|
||||
SET_CID_DEFAULT(x_cid_auto, x_field##_auto, false); \
|
||||
} \
|
||||
}
|
||||
|
||||
CONTROL_AUTO_CID (V4L2_CID_AUTOBRIGHTNESS, V4L2_CID_BRIGHTNESS, brightness);
|
||||
CONTROL_MANUAL_CID ( V4L2_CID_CONTRAST, contrast);
|
||||
CONTROL_MANUAL_CID ( V4L2_CID_SATURATION, saturation);
|
||||
CONTROL_AUTO_CID (V4L2_CID_HUE_AUTO, V4L2_CID_HUE, hue);
|
||||
CONTROL_MANUAL_CID ( V4L2_CID_GAMMA, gamma);
|
||||
CONTROL_MANUAL_CID ( V4L2_CID_SHARPNESS, sharpness);
|
||||
CONTROL_MANUAL_CID ( V4L2_CID_BACKLIGHT_COMPENSATION, backlight_compensation);
|
||||
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);
|
||||
|
||||
# undef CONTROL_AUTO_CID
|
||||
# undef CONTROL_MANUAL_CID
|
||||
# undef SET_CID_DEFAULT
|
||||
# undef SET_CID_VALUE
|
||||
}
|
||||
|
||||
static int _query_control(
|
||||
int fd,
|
||||
struct v4l2_queryctrl *query,
|
||||
const char *name,
|
||||
uint cid,
|
||||
bool quiet
|
||||
) {
|
||||
// cppcheck-suppress redundantPointerOp
|
||||
US_MEMSET_ZERO(*query);
|
||||
query->id = cid;
|
||||
|
||||
if (us_xioctl(fd, VIDIOC_QUERYCTRL, query) < 0 || query->flags & V4L2_CTRL_FLAG_DISABLED) {
|
||||
if (!quiet) {
|
||||
_LOG_ERROR("Changing control %s is unsupported", name);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void _set_control(
|
||||
int fd,
|
||||
const struct v4l2_queryctrl *query,
|
||||
const char *name,
|
||||
uint cid,
|
||||
int value,
|
||||
bool quiet
|
||||
) {
|
||||
if (value < query->minimum || value > query->maximum || value % query->step != 0) {
|
||||
if (!quiet) {
|
||||
_LOG_ERROR("Invalid value %d of control %s: min=%d, max=%d, default=%d, step=%u",
|
||||
value, name, query->minimum, query->maximum, query->default_value, query->step);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
struct v4l2_control ctl = {
|
||||
.id = cid,
|
||||
.value = value,
|
||||
};
|
||||
if (us_xioctl(fd, VIDIOC_S_CTRL, &ctl) < 0) {
|
||||
if (!quiet) {
|
||||
_LOG_PERROR("Can't set control %s", name);
|
||||
}
|
||||
} else if (!quiet) {
|
||||
_LOG_INFO("Applying control %s: %d", name, ctl.value);
|
||||
}
|
||||
}
|
||||
60
src/libs/controls.h
Normal file
60
src/libs/controls.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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 "types.h"
|
||||
|
||||
|
||||
typedef enum {
|
||||
US_CTL_MODE_NONE = 0,
|
||||
US_CTL_MODE_VALUE,
|
||||
US_CTL_MODE_AUTO,
|
||||
US_CTL_MODE_DEFAULT,
|
||||
} us_control_mode_e;
|
||||
|
||||
typedef struct {
|
||||
us_control_mode_e mode;
|
||||
int value;
|
||||
} us_control_s;
|
||||
|
||||
typedef struct {
|
||||
us_control_s brightness;
|
||||
us_control_s contrast;
|
||||
us_control_s saturation;
|
||||
us_control_s hue;
|
||||
us_control_s gamma;
|
||||
us_control_s sharpness;
|
||||
us_control_s backlight_compensation;
|
||||
us_control_s white_balance;
|
||||
us_control_s gain;
|
||||
us_control_s color_effect;
|
||||
us_control_s rotate;
|
||||
us_control_s flip_vertical;
|
||||
us_control_s flip_horizontal;
|
||||
} us_controls_s;
|
||||
|
||||
|
||||
us_controls_s *us_controls_init(void);
|
||||
void us_controls_destroy(us_controls_s *ctl);
|
||||
|
||||
void us_controls_apply(const us_controls_s *ctl, int fd);
|
||||
794
src/libs/drm/drm.c
Normal file
794
src/libs/drm/drm.c
Normal file
@@ -0,0 +1,794 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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 "drm.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#ifdef __linux__
|
||||
# include <sys/sysmacros.h>
|
||||
#endif
|
||||
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#include <xf86drm.h>
|
||||
#include <xf86drmMode.h>
|
||||
#include <drm_fourcc.h>
|
||||
#include <libdrm/drm.h>
|
||||
|
||||
#include "../types.h"
|
||||
#include "../errors.h"
|
||||
#include "../tools.h"
|
||||
#include "../logging.h"
|
||||
#include "../frame.h"
|
||||
#include "../frametext.h"
|
||||
#include "../capture.h"
|
||||
|
||||
|
||||
static void _drm_vsync_callback(int fd, uint n_frame, uint sec, uint usec, void *v_buf);
|
||||
static int _drm_check_status(us_drm_s *drm);
|
||||
static void _drm_ensure_dpms_power(us_drm_s *drm, bool on);
|
||||
static int _drm_init_buffers(us_drm_s *drm, const us_capture_s *cap);
|
||||
static int _drm_find_sink(us_drm_s *drm, uint width, uint height, float hz);
|
||||
|
||||
static drmModeModeInfo *_find_best_mode(drmModeConnector *conn, uint width, uint height, float hz);
|
||||
static u32 _find_dpms(int fd, drmModeConnector *conn);
|
||||
static u32 _find_crtc(int fd, const drmModeRes *res, drmModeConnector *conn, u32 *taken_crtcs);
|
||||
static const char *_connector_type_to_string(u32 type);
|
||||
static float _get_refresh_rate(const drmModeModeInfo *mode);
|
||||
|
||||
|
||||
#define _LOG_ERROR(x_msg, ...) US_LOG_ERROR("DRM: " x_msg, ##__VA_ARGS__)
|
||||
#define _LOG_PERROR(x_msg, ...) US_LOG_PERROR("DRM: " x_msg, ##__VA_ARGS__)
|
||||
#define _LOG_INFO(x_msg, ...) US_LOG_INFO("DRM: " x_msg, ##__VA_ARGS__)
|
||||
#define _LOG_VERBOSE(x_msg, ...) US_LOG_VERBOSE("DRM: " x_msg, ##__VA_ARGS__)
|
||||
#define _LOG_DEBUG(x_msg, ...) US_LOG_DEBUG("DRM: " x_msg, ##__VA_ARGS__)
|
||||
|
||||
|
||||
us_drm_s *us_drm_init(void) {
|
||||
us_drm_runtime_s *run;
|
||||
US_CALLOC(run, 1);
|
||||
run->fd = -1;
|
||||
run->status_fd = -1;
|
||||
run->dpms_state = -1;
|
||||
run->opened = -1;
|
||||
run->has_vsync = true;
|
||||
run->exposing_dma_fd = -1;
|
||||
run->ft = us_frametext_init();
|
||||
|
||||
us_drm_s *drm;
|
||||
US_CALLOC(drm, 1);
|
||||
// drm->path = "/dev/dri/card0";
|
||||
drm->path = "/dev/dri/by-path/platform-gpu-card";
|
||||
drm->port = "HDMI-A-2"; // OUT2 on PiKVM V4 Plus
|
||||
drm->timeout = 5;
|
||||
drm->blank_after = 5;
|
||||
drm->run = run;
|
||||
return drm;
|
||||
}
|
||||
|
||||
void us_drm_destroy(us_drm_s *drm) {
|
||||
us_frametext_destroy(drm->run->ft);
|
||||
US_DELETE(drm->run, free);
|
||||
US_DELETE(drm, free); // cppcheck-suppress uselessAssignmentPtrArg
|
||||
}
|
||||
|
||||
int us_drm_open(us_drm_s *drm, const us_capture_s *cap) {
|
||||
us_drm_runtime_s *const run = drm->run;
|
||||
|
||||
US_A(run->fd < 0);
|
||||
|
||||
switch (_drm_check_status(drm)) {
|
||||
case 0: break;
|
||||
case US_ERROR_NO_DEVICE: goto unplugged;
|
||||
default: goto error;
|
||||
}
|
||||
|
||||
_LOG_INFO("Using passthrough: %s[%s]", drm->path, drm->port);
|
||||
_LOG_INFO("Configuring DRM device for %s ...", (cap == NULL ? "STUB" : "DMA"));
|
||||
|
||||
if ((run->fd = open(drm->path, O_RDWR | O_CLOEXEC | O_NONBLOCK)) < 0) {
|
||||
_LOG_PERROR("Can't open DRM device");
|
||||
goto error;
|
||||
}
|
||||
_LOG_DEBUG("DRM device fd=%d opened", run->fd);
|
||||
|
||||
int stub = 0; // Open the real device with DMA
|
||||
if (cap == NULL) {
|
||||
stub = US_DRM_STUB_USER;
|
||||
} else if (cap->run->format != V4L2_PIX_FMT_RGB24 && cap->run->format != V4L2_PIX_FMT_BGR24) {
|
||||
stub = US_DRM_STUB_BAD_FORMAT;
|
||||
char fourcc_str[8];
|
||||
us_fourcc_to_string(cap->run->format, fourcc_str, 8);
|
||||
_LOG_ERROR("Input format %s is not supported, forcing to STUB ...", fourcc_str);
|
||||
}
|
||||
|
||||
# define CHECK_CAP(x_cap) { \
|
||||
_LOG_DEBUG("Checking %s ...", #x_cap); \
|
||||
u64 m_check; \
|
||||
if (drmGetCap(run->fd, x_cap, &m_check) < 0) { \
|
||||
_LOG_PERROR("Can't check " #x_cap); \
|
||||
goto error; \
|
||||
} \
|
||||
if (!m_check) { \
|
||||
_LOG_ERROR(#x_cap " is not supported"); \
|
||||
goto error; \
|
||||
} \
|
||||
}
|
||||
CHECK_CAP(DRM_CAP_DUMB_BUFFER);
|
||||
if (stub == 0) {
|
||||
CHECK_CAP(DRM_CAP_PRIME);
|
||||
}
|
||||
# undef CHECK_CAP
|
||||
|
||||
const uint width = (stub > 0 ? 0 : cap->run->width);
|
||||
const uint height = (stub > 0 ? 0 : cap->run->height);
|
||||
const uint hz = (stub > 0 ? 0 : cap->run->hz);
|
||||
switch (_drm_find_sink(drm, width, height, hz)) {
|
||||
case 0: break;
|
||||
case US_ERROR_NO_DEVICE: goto unplugged;
|
||||
default: goto error;
|
||||
}
|
||||
if ((stub == 0) && (width != run->mode.hdisplay || height < run->mode.vdisplay)) {
|
||||
// We'll try to show something instead of nothing if height != vdisplay
|
||||
stub = US_DRM_STUB_BAD_RESOLUTION;
|
||||
_LOG_ERROR("There is no appropriate modes for the capture, forcing to STUB ...");
|
||||
}
|
||||
|
||||
if (_drm_init_buffers(drm, (stub > 0 ? NULL : cap)) < 0) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
run->saved_crtc = drmModeGetCrtc(run->fd, run->crtc_id);
|
||||
_LOG_DEBUG("Setting up CRTC ...");
|
||||
if (drmModeSetCrtc(
|
||||
run->fd,
|
||||
run->crtc_id, run->bufs[0].id,
|
||||
0, 0, // X, Y
|
||||
&run->conn_id, 1, &run->mode
|
||||
) < 0) {
|
||||
_LOG_PERROR("Can't set CRTC");
|
||||
goto error;
|
||||
}
|
||||
|
||||
_LOG_INFO("Opened for %s ...", (stub > 0 ? "STUB" : "DMA"));
|
||||
run->exposing_dma_fd = -1;
|
||||
run->blank_at_ts = 0;
|
||||
run->opened = stub;
|
||||
run->once = 0;
|
||||
return run->opened;
|
||||
|
||||
error:
|
||||
us_drm_close(drm);
|
||||
return run->opened; // -1 after us_drm_close()
|
||||
|
||||
unplugged:
|
||||
US_ONCE_FOR(run->once, __LINE__, {
|
||||
_LOG_ERROR("Display is not plugged");
|
||||
});
|
||||
us_drm_close(drm);
|
||||
run->opened = US_ERROR_NO_DEVICE;
|
||||
return run->opened;
|
||||
}
|
||||
|
||||
void us_drm_close(us_drm_s *drm) {
|
||||
us_drm_runtime_s *const run = drm->run;
|
||||
|
||||
if (run->exposing_dma_fd >= 0) {
|
||||
// Нужно подождать, пока dma_fd не освободится, прежде чем прерывать процесс.
|
||||
// Просто на всякий случай.
|
||||
US_A(run->fd >= 0);
|
||||
us_drm_wait_for_vsync(drm);
|
||||
run->exposing_dma_fd = -1;
|
||||
}
|
||||
|
||||
if (run->saved_crtc != NULL) {
|
||||
_LOG_DEBUG("Restoring CRTC ...");
|
||||
if (drmModeSetCrtc(
|
||||
run->fd,
|
||||
run->saved_crtc->crtc_id, run->saved_crtc->buffer_id,
|
||||
run->saved_crtc->x, run->saved_crtc->y,
|
||||
&run->conn_id, 1, &run->saved_crtc->mode
|
||||
) < 0 && errno != ENOENT) {
|
||||
_LOG_PERROR("Can't restore CRTC");
|
||||
}
|
||||
drmModeFreeCrtc(run->saved_crtc);
|
||||
run->saved_crtc = NULL;
|
||||
}
|
||||
|
||||
if (run->bufs != NULL) {
|
||||
_LOG_DEBUG("Releasing buffers ...");
|
||||
for (uint n_buf = 0; n_buf < run->n_bufs; ++n_buf) {
|
||||
us_drm_buffer_s *const buf = &run->bufs[n_buf];
|
||||
if (buf->fb_added && drmModeRmFB(run->fd, buf->id) < 0) {
|
||||
_LOG_PERROR("Can't remove buffer=%u", n_buf);
|
||||
}
|
||||
if (buf->dumb_created) {
|
||||
struct drm_mode_destroy_dumb destroy = {.handle = buf->handle};
|
||||
if (drmIoctl(run->fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy) < 0) {
|
||||
_LOG_PERROR("Can't destroy dumb buffer=%u", n_buf);
|
||||
}
|
||||
}
|
||||
if (buf->data != NULL && munmap(buf->data, buf->allocated)) {
|
||||
_LOG_PERROR("Can't unmap buffer=%u", n_buf);
|
||||
}
|
||||
}
|
||||
US_DELETE(run->bufs, free);
|
||||
run->n_bufs = 0;
|
||||
}
|
||||
|
||||
const bool say = (run->fd >= 0);
|
||||
US_CLOSE_FD(run->status_fd);
|
||||
US_CLOSE_FD(run->fd);
|
||||
|
||||
run->crtc_id = 0;
|
||||
run->dpms_state = -1;
|
||||
run->opened = -1;
|
||||
run->has_vsync = true;
|
||||
run->stub_n_buf = 0;
|
||||
|
||||
if (say) {
|
||||
_LOG_INFO("Closed");
|
||||
}
|
||||
}
|
||||
|
||||
int us_drm_ensure_no_signal(us_drm_s *drm) {
|
||||
us_drm_runtime_s *const run = drm->run;
|
||||
|
||||
US_A(run->fd >= 0);
|
||||
US_A(run->opened > 0);
|
||||
|
||||
const ldf now_ts = us_get_now_monotonic();
|
||||
if (run->blank_at_ts == 0) {
|
||||
run->blank_at_ts = now_ts + drm->blank_after;
|
||||
}
|
||||
const ldf saved_ts = run->blank_at_ts; // us_drm*() rewrites it to 0
|
||||
|
||||
int retval;
|
||||
if (now_ts <= run->blank_at_ts) {
|
||||
retval = us_drm_wait_for_vsync(drm);
|
||||
if (retval == 0) {
|
||||
retval = us_drm_expose_stub(drm, US_DRM_STUB_NO_SIGNAL, NULL);
|
||||
}
|
||||
} else {
|
||||
US_ONCE_FOR(run->once, __LINE__, {
|
||||
_LOG_INFO("Turning off the display by timeout ...");
|
||||
});
|
||||
retval = us_drm_dpms_power_off(drm);
|
||||
}
|
||||
run->blank_at_ts = saved_ts;
|
||||
return retval;
|
||||
}
|
||||
|
||||
int us_drm_dpms_power_off(us_drm_s *drm) {
|
||||
US_A(drm->run->fd >= 0);
|
||||
switch (_drm_check_status(drm)) {
|
||||
case 0: break;
|
||||
case US_ERROR_NO_DEVICE: return 0; // Unplugged, nice
|
||||
// Во время переключения DPMS монитор моргает один раз состоянием disconnected,
|
||||
// а потом почему-то снова оказывается connected. Так что просто считаем,
|
||||
// что отсоединенный монитор на этом этапе - это нормально.
|
||||
default: return -1;
|
||||
}
|
||||
_drm_ensure_dpms_power(drm, false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int us_drm_wait_for_vsync(us_drm_s *drm) {
|
||||
us_drm_runtime_s *const run = drm->run;
|
||||
|
||||
US_A(run->fd >= 0);
|
||||
run->blank_at_ts = 0;
|
||||
|
||||
switch (_drm_check_status(drm)) {
|
||||
case 0: break;
|
||||
case US_ERROR_NO_DEVICE: return US_ERROR_NO_DEVICE;
|
||||
default: return -1;
|
||||
}
|
||||
_drm_ensure_dpms_power(drm, true);
|
||||
|
||||
if (run->has_vsync) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct timeval timeout = {.tv_sec = drm->timeout};
|
||||
fd_set fds;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(run->fd, &fds);
|
||||
|
||||
_LOG_DEBUG("Calling select() for VSync ...");
|
||||
const int result = select(run->fd + 1, &fds, NULL, NULL, &timeout);
|
||||
if (result < 0) {
|
||||
_LOG_PERROR("Can't select(%d) device for VSync", run->fd);
|
||||
return -1;
|
||||
} else if (result == 0) {
|
||||
_LOG_ERROR("Device timeout while waiting VSync");
|
||||
return -1;
|
||||
}
|
||||
|
||||
drmEventContext ctx = {
|
||||
.version = DRM_EVENT_CONTEXT_VERSION,
|
||||
.page_flip_handler = _drm_vsync_callback,
|
||||
};
|
||||
_LOG_DEBUG("Handling DRM event (maybe VSync) ...");
|
||||
if (drmHandleEvent(run->fd, &ctx) < 0) {
|
||||
_LOG_PERROR("Can't handle DRM event");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void _drm_vsync_callback(int fd, uint n_frame, uint sec, uint usec, void *v_buf) {
|
||||
(void)fd;
|
||||
(void)n_frame;
|
||||
(void)sec;
|
||||
(void)usec;
|
||||
us_drm_buffer_s *const buf = v_buf;
|
||||
*buf->ctx.has_vsync = true;
|
||||
*buf->ctx.exposing_dma_fd = -1;
|
||||
_LOG_DEBUG("Got VSync signal");
|
||||
}
|
||||
|
||||
int us_drm_expose_stub(us_drm_s *drm, us_drm_stub_e stub, const us_capture_s *cap) {
|
||||
us_drm_runtime_s *const run = drm->run;
|
||||
|
||||
US_A(run->fd >= 0);
|
||||
US_A(run->opened > 0);
|
||||
run->blank_at_ts = 0;
|
||||
|
||||
switch (_drm_check_status(drm)) {
|
||||
case 0: break;
|
||||
case US_ERROR_NO_DEVICE: return US_ERROR_NO_DEVICE;
|
||||
default: return -1;
|
||||
}
|
||||
_drm_ensure_dpms_power(drm, true);
|
||||
|
||||
# define DRAW_MSG(x_msg) us_frametext_draw(run->ft, (x_msg), run->mode.hdisplay, run->mode.vdisplay)
|
||||
switch (stub) {
|
||||
case US_DRM_STUB_BAD_RESOLUTION: {
|
||||
US_A(cap != NULL);
|
||||
char msg[1024];
|
||||
US_SNPRINTF(msg, 1023,
|
||||
"=== PiKVM ==="
|
||||
"\n \n< UNSUPPORTED RESOLUTION >"
|
||||
"\n \n< %ux%up%.02f >"
|
||||
"\n \nby this display",
|
||||
cap->run->width, cap->run->height, cap->run->hz);
|
||||
DRAW_MSG(msg);
|
||||
break;
|
||||
};
|
||||
case US_DRM_STUB_BAD_FORMAT:
|
||||
DRAW_MSG("=== PiKVM ===\n \n< UNSUPPORTED CAPTURE FORMAT >");
|
||||
break;
|
||||
case US_DRM_STUB_NO_SIGNAL:
|
||||
DRAW_MSG("=== PiKVM ===\n \n< NO LIVE VIDEO >");
|
||||
break;
|
||||
case US_DRM_STUB_BUSY:
|
||||
DRAW_MSG("=== PiKVM ===\n \n< ONLINE IS ACTIVE >");
|
||||
break;
|
||||
default:
|
||||
DRAW_MSG("=== PiKVM ===\n \n< ??? >");
|
||||
break;
|
||||
}
|
||||
# undef DRAW_MSG
|
||||
|
||||
us_drm_buffer_s *const buf = &run->bufs[run->stub_n_buf];
|
||||
|
||||
run->has_vsync = false;
|
||||
|
||||
_LOG_DEBUG("Copying STUB frame ...")
|
||||
memcpy(buf->data, run->ft->frame->data, US_MIN(run->ft->frame->used, buf->allocated));
|
||||
|
||||
_LOG_DEBUG("Exposing STUB framebuffer n_buf=%u ...", run->stub_n_buf);
|
||||
const int retval = drmModePageFlip(
|
||||
run->fd, run->crtc_id, buf->id,
|
||||
DRM_MODE_PAGE_FLIP_EVENT | DRM_MODE_PAGE_FLIP_ASYNC,
|
||||
buf);
|
||||
if (retval < 0) {
|
||||
_LOG_PERROR("Can't expose STUB framebuffer n_buf=%u ...", run->stub_n_buf);
|
||||
}
|
||||
_LOG_DEBUG("Exposed STUB framebuffer n_buf=%u", run->stub_n_buf);
|
||||
|
||||
run->stub_n_buf = (run->stub_n_buf + 1) % run->n_bufs;
|
||||
return retval;
|
||||
}
|
||||
|
||||
int us_drm_expose_dma(us_drm_s *drm, const us_capture_hwbuf_s *hw) {
|
||||
us_drm_runtime_s *const run = drm->run;
|
||||
us_drm_buffer_s *const buf = &run->bufs[hw->buf.index];
|
||||
|
||||
US_A(run->fd >= 0);
|
||||
US_A(run->opened == 0);
|
||||
run->blank_at_ts = 0;
|
||||
|
||||
switch (_drm_check_status(drm)) {
|
||||
case 0: break;
|
||||
case US_ERROR_NO_DEVICE: return US_ERROR_NO_DEVICE;
|
||||
default: return -1;
|
||||
}
|
||||
_drm_ensure_dpms_power(drm, true);
|
||||
|
||||
run->has_vsync = false;
|
||||
|
||||
_LOG_DEBUG("Exposing DMA framebuffer n_buf=%u ...", hw->buf.index);
|
||||
const int retval = drmModePageFlip(
|
||||
run->fd, run->crtc_id, buf->id,
|
||||
DRM_MODE_PAGE_FLIP_EVENT | DRM_MODE_PAGE_FLIP_ASYNC,
|
||||
buf);
|
||||
if (retval < 0) {
|
||||
_LOG_PERROR("Can't expose DMA framebuffer n_buf=%u ...", run->stub_n_buf);
|
||||
}
|
||||
_LOG_DEBUG("Exposed DMA framebuffer n_buf=%u", run->stub_n_buf);
|
||||
run->exposing_dma_fd = hw->dma_fd;
|
||||
return retval;
|
||||
}
|
||||
|
||||
static int _drm_check_status(us_drm_s *drm) {
|
||||
us_drm_runtime_s *run = drm->run;
|
||||
|
||||
if (run->status_fd < 0) {
|
||||
_LOG_DEBUG("Trying to find status file ...");
|
||||
struct stat st;
|
||||
if (stat(drm->path, &st) < 0) {
|
||||
_LOG_PERROR("Can't stat() DRM device");
|
||||
goto error;
|
||||
}
|
||||
const uint mi = minor(st.st_rdev);
|
||||
_LOG_DEBUG("DRM device minor(st_rdev)=%u", mi);
|
||||
|
||||
char path[128];
|
||||
US_SNPRINTF(path, 127, "/sys/class/drm/card%u-%s/status", mi, drm->port);
|
||||
_LOG_DEBUG("Opening status file %s ...", path);
|
||||
if ((run->status_fd = open(path, O_RDONLY | O_CLOEXEC)) < 0) {
|
||||
_LOG_PERROR("Can't open status file: %s", path);
|
||||
goto error;
|
||||
}
|
||||
_LOG_DEBUG("Status file fd=%d opened", run->status_fd);
|
||||
}
|
||||
|
||||
char status_ch;
|
||||
if (read(run->status_fd, &status_ch, 1) != 1) {
|
||||
_LOG_PERROR("Can't read status file");
|
||||
goto error;
|
||||
}
|
||||
if (lseek(run->status_fd, 0, SEEK_SET) != 0) {
|
||||
_LOG_PERROR("Can't rewind status file");
|
||||
goto error;
|
||||
}
|
||||
_LOG_DEBUG("Current display status: %c", status_ch);
|
||||
return (status_ch == 'd' ? US_ERROR_NO_DEVICE : 0);
|
||||
|
||||
error:
|
||||
US_CLOSE_FD(run->status_fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void _drm_ensure_dpms_power(us_drm_s *drm, bool on) {
|
||||
us_drm_runtime_s *const run = drm->run;
|
||||
if (run->dpms_id > 0 && run->dpms_state != (int)on) {
|
||||
_LOG_INFO("Changing DPMS power mode: %d -> %u ...", run->dpms_state, on);
|
||||
if (drmModeConnectorSetProperty(
|
||||
run->fd, run->conn_id, run->dpms_id,
|
||||
(on ? DRM_MODE_DPMS_ON : DRM_MODE_DPMS_OFF)
|
||||
) < 0) {
|
||||
_LOG_PERROR("Can't set DPMS power=%u (ignored)", on);
|
||||
}
|
||||
}
|
||||
run->dpms_state = (int)on;
|
||||
}
|
||||
|
||||
static int _drm_init_buffers(us_drm_s *drm, const us_capture_s *cap) {
|
||||
us_drm_runtime_s *const run = drm->run;
|
||||
|
||||
const uint n_bufs = (cap == NULL ? 4 : cap->run->n_bufs);
|
||||
const char *name = (cap == NULL ? "STUB" : "DMA");
|
||||
|
||||
_LOG_DEBUG("Initializing %u %s buffers ...", n_bufs, name);
|
||||
|
||||
uint format = DRM_FORMAT_RGB888;
|
||||
|
||||
US_CALLOC(run->bufs, n_bufs);
|
||||
for (run->n_bufs = 0; run->n_bufs < n_bufs; ++run->n_bufs) {
|
||||
const uint n_buf = run->n_bufs;
|
||||
us_drm_buffer_s *const buf = &run->bufs[n_buf];
|
||||
|
||||
buf->ctx.has_vsync = &run->has_vsync;
|
||||
buf->ctx.exposing_dma_fd = &run->exposing_dma_fd;
|
||||
|
||||
u32 handles[4] = {0};
|
||||
u32 strides[4] = {0};
|
||||
u32 offsets[4] = {0};
|
||||
|
||||
if (cap == NULL) {
|
||||
struct drm_mode_create_dumb create = {
|
||||
.width = run->mode.hdisplay,
|
||||
.height = run->mode.vdisplay,
|
||||
.bpp = 24,
|
||||
};
|
||||
if (drmIoctl(run->fd, DRM_IOCTL_MODE_CREATE_DUMB, &create) < 0) {
|
||||
_LOG_PERROR("Can't create %s buffer=%u", name, n_buf);
|
||||
return -1;
|
||||
}
|
||||
buf->handle = create.handle;
|
||||
buf->dumb_created = true;
|
||||
|
||||
struct drm_mode_map_dumb map = {.handle = create.handle};
|
||||
if (drmIoctl(run->fd, DRM_IOCTL_MODE_MAP_DUMB, &map) < 0) {
|
||||
_LOG_PERROR("Can't prepare dumb buffer=%u to mapping", n_buf);
|
||||
return -1;
|
||||
}
|
||||
if ((buf->data = mmap(
|
||||
NULL, create.size,
|
||||
PROT_READ | PROT_WRITE, MAP_SHARED,
|
||||
run->fd, map.offset
|
||||
)) == MAP_FAILED) {
|
||||
_LOG_PERROR("Can't map buffer=%u", n_buf);
|
||||
return -1;
|
||||
}
|
||||
memset(buf->data, 0, create.size);
|
||||
buf->allocated = create.size;
|
||||
|
||||
handles[0] = create.handle;
|
||||
strides[0] = create.pitch;
|
||||
|
||||
} else {
|
||||
if (drmPrimeFDToHandle(run->fd, cap->run->bufs[n_buf].dma_fd, &buf->handle) < 0) {
|
||||
_LOG_PERROR("Can't import DMA buffer=%u from capture device", n_buf);
|
||||
return -1;
|
||||
}
|
||||
handles[0] = buf->handle;
|
||||
strides[0] = cap->run->stride;
|
||||
|
||||
switch (cap->run->format) {
|
||||
case V4L2_PIX_FMT_RGB24: format = (DRM_FORMAT_BIG_ENDIAN ? DRM_FORMAT_BGR888 : DRM_FORMAT_RGB888); break;
|
||||
case V4L2_PIX_FMT_BGR24: format = (DRM_FORMAT_BIG_ENDIAN ? DRM_FORMAT_RGB888 : DRM_FORMAT_BGR888); break;
|
||||
}
|
||||
}
|
||||
|
||||
if (drmModeAddFB2(
|
||||
run->fd,
|
||||
run->mode.hdisplay, run->mode.vdisplay, format,
|
||||
handles, strides, offsets, &buf->id, 0
|
||||
)) {
|
||||
_LOG_PERROR("Can't setup buffer=%u", n_buf);
|
||||
return -1;
|
||||
}
|
||||
buf->fb_added = true;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int _drm_find_sink(us_drm_s *drm, uint width, uint height, float hz) {
|
||||
us_drm_runtime_s *const run = drm->run;
|
||||
|
||||
run->crtc_id = 0;
|
||||
|
||||
_LOG_DEBUG("Trying to find the appropriate sink ...");
|
||||
|
||||
drmModeRes *res = drmModeGetResources(run->fd);
|
||||
if (res == NULL) {
|
||||
_LOG_PERROR("Can't get resources info");
|
||||
goto done;
|
||||
}
|
||||
if (res->count_connectors <= 0) {
|
||||
_LOG_ERROR("Can't find any connectors");
|
||||
goto done;
|
||||
}
|
||||
|
||||
for (int ci = 0; ci < res->count_connectors; ++ci) {
|
||||
drmModeConnector *conn = drmModeGetConnector(run->fd, res->connectors[ci]);
|
||||
if (conn == NULL) {
|
||||
_LOG_PERROR("Can't get connector index=%d", ci);
|
||||
goto done;
|
||||
}
|
||||
|
||||
char port[32];
|
||||
US_SNPRINTF(port, 31, "%s-%u",
|
||||
_connector_type_to_string(conn->connector_type),
|
||||
conn->connector_type_id);
|
||||
if (strcmp(port, drm->port) != 0) {
|
||||
drmModeFreeConnector(conn);
|
||||
continue;
|
||||
}
|
||||
_LOG_INFO("Using connector %s: conn_type=%d, conn_type_id=%d",
|
||||
drm->port, conn->connector_type, conn->connector_type_id);
|
||||
|
||||
if (conn->connection != DRM_MODE_CONNECTED) {
|
||||
_LOG_ERROR("Connector for port %s has !DRM_MODE_CONNECTED", drm->port);
|
||||
drmModeFreeConnector(conn);
|
||||
goto done;
|
||||
}
|
||||
|
||||
const drmModeModeInfo *best;
|
||||
if ((best = _find_best_mode(conn, width, height, hz)) == NULL) {
|
||||
_LOG_ERROR("Can't find any appropriate display modes");
|
||||
drmModeFreeConnector(conn);
|
||||
goto unplugged;
|
||||
}
|
||||
_LOG_INFO("Using best mode: %ux%up%.02f",
|
||||
best->hdisplay, best->vdisplay, _get_refresh_rate(best));
|
||||
|
||||
if ((run->dpms_id = _find_dpms(run->fd, conn)) > 0) {
|
||||
_LOG_INFO("Using DPMS: id=%u", run->dpms_id);
|
||||
} else {
|
||||
_LOG_INFO("Using DPMS: None");
|
||||
}
|
||||
|
||||
u32 taken_crtcs = 0; // Unused here
|
||||
if ((run->crtc_id = _find_crtc(run->fd, res, conn, &taken_crtcs)) == 0) {
|
||||
_LOG_ERROR("Can't find CRTC");
|
||||
drmModeFreeConnector(conn);
|
||||
goto done;
|
||||
}
|
||||
_LOG_INFO("Using CRTC: id=%u", run->crtc_id);
|
||||
|
||||
run->conn_id = conn->connector_id;
|
||||
memcpy(&run->mode, best, sizeof(drmModeModeInfo));
|
||||
|
||||
drmModeFreeConnector(conn);
|
||||
break;
|
||||
}
|
||||
|
||||
done:
|
||||
drmModeFreeResources(res);
|
||||
return (run->crtc_id > 0 ? 0 : -1);
|
||||
|
||||
unplugged:
|
||||
drmModeFreeResources(res);
|
||||
return US_ERROR_NO_DEVICE;
|
||||
}
|
||||
|
||||
static drmModeModeInfo *_find_best_mode(drmModeConnector *conn, uint width, uint height, float hz) {
|
||||
drmModeModeInfo *best = NULL;
|
||||
drmModeModeInfo *closest = NULL;
|
||||
drmModeModeInfo *pref = NULL;
|
||||
|
||||
for (int mi = 0; mi < conn->count_modes; ++mi) {
|
||||
drmModeModeInfo *const mode = &conn->modes[mi];
|
||||
if (mode->flags & DRM_MODE_FLAG_INTERLACE) {
|
||||
continue; // Discard interlaced
|
||||
}
|
||||
const float mode_hz = _get_refresh_rate(mode);
|
||||
if (width == 640 && height == 416 && mode->hdisplay == 640 && mode->vdisplay == 480) {
|
||||
// A special case for some ancient DOS device with VGA converter.
|
||||
// @CapnKirk in Discord
|
||||
if (hz > 0 && mode_hz < hz) {
|
||||
best = mode;
|
||||
best->vdisplay = 416;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (mode->hdisplay == width && mode->vdisplay == height) {
|
||||
best = mode; // Any mode with exact resolution
|
||||
if (hz > 0 && mode_hz == hz) {
|
||||
break; // Exact mode with same freq
|
||||
}
|
||||
}
|
||||
if (mode->hdisplay == width && mode->vdisplay < height) {
|
||||
if (closest == NULL || _get_refresh_rate(closest) != hz) {
|
||||
closest = mode; // Something like 1920x1080p60 for 1920x1200p60 source
|
||||
}
|
||||
}
|
||||
if (pref == NULL && (mode->type & DRM_MODE_TYPE_PREFERRED)) {
|
||||
pref = mode; // Preferred mode if nothing is found
|
||||
}
|
||||
}
|
||||
|
||||
if (best == NULL) {
|
||||
best = closest;
|
||||
}
|
||||
if (best == NULL) {
|
||||
best = pref;
|
||||
}
|
||||
if (best == NULL) {
|
||||
best = (conn->count_modes > 0 ? &conn->modes[0] : NULL);
|
||||
}
|
||||
US_A(best == NULL || best->hdisplay > 0);
|
||||
US_A(best == NULL || best->vdisplay > 0);
|
||||
return best;
|
||||
}
|
||||
|
||||
static u32 _find_dpms(int fd, drmModeConnector *conn) {
|
||||
for (int pi = 0; pi < conn->count_props; pi++) {
|
||||
drmModePropertyPtr prop = drmModeGetProperty(fd, conn->props[pi]);
|
||||
if (prop != NULL) {
|
||||
if (!strcmp(prop->name, "DPMS")) {
|
||||
const u32 id = prop->prop_id;
|
||||
drmModeFreeProperty(prop);
|
||||
return id;
|
||||
}
|
||||
drmModeFreeProperty(prop);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static u32 _find_crtc(int fd, const drmModeRes *res, drmModeConnector *conn, u32 *taken_crtcs) {
|
||||
for (int ei = 0; ei < conn->count_encoders; ++ei) {
|
||||
drmModeEncoder *enc = drmModeGetEncoder(fd, conn->encoders[ei]);
|
||||
if (enc == NULL) {
|
||||
continue;
|
||||
}
|
||||
for (int ci = 0; ci < res->count_crtcs; ++ci) {
|
||||
u32 bit = (1 << ci);
|
||||
if (!(enc->possible_crtcs & bit)) {
|
||||
continue; // Not compatible
|
||||
}
|
||||
if (*taken_crtcs & bit) {
|
||||
continue; // Already taken
|
||||
}
|
||||
drmModeFreeEncoder(enc);
|
||||
*taken_crtcs |= bit;
|
||||
return res->crtcs[ci];
|
||||
}
|
||||
drmModeFreeEncoder(enc);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const char *_connector_type_to_string(u32 type) {
|
||||
switch (type) {
|
||||
# define CASE_NAME(x_suffix, x_name) \
|
||||
case DRM_MODE_CONNECTOR_##x_suffix: return x_name;
|
||||
CASE_NAME(VGA, "VGA");
|
||||
CASE_NAME(DVII, "DVI-I");
|
||||
CASE_NAME(DVID, "DVI-D");
|
||||
CASE_NAME(DVIA, "DVI-A");
|
||||
CASE_NAME(Composite, "Composite");
|
||||
CASE_NAME(SVIDEO, "SVIDEO");
|
||||
CASE_NAME(LVDS, "LVDS");
|
||||
CASE_NAME(Component, "Component");
|
||||
CASE_NAME(9PinDIN, "DIN");
|
||||
CASE_NAME(DisplayPort, "DP");
|
||||
CASE_NAME(HDMIA, "HDMI-A");
|
||||
CASE_NAME(HDMIB, "HDMI-B");
|
||||
CASE_NAME(TV, "TV");
|
||||
CASE_NAME(eDP, "eDP");
|
||||
CASE_NAME(VIRTUAL, "Virtual");
|
||||
CASE_NAME(DSI, "DSI");
|
||||
CASE_NAME(DPI, "DPI");
|
||||
CASE_NAME(WRITEBACK, "Writeback");
|
||||
CASE_NAME(SPI, "SPI");
|
||||
CASE_NAME(USB, "USB");
|
||||
case DRM_MODE_CONNECTOR_Unknown: break;
|
||||
# undef CASE_NAME
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
static float _get_refresh_rate(const drmModeModeInfo *mode) {
|
||||
int mhz = (mode->clock * 1000000LL / mode->htotal + mode->vtotal / 2) / mode->vtotal;
|
||||
if (mode->flags & DRM_MODE_FLAG_INTERLACE) {
|
||||
mhz *= 2;
|
||||
}
|
||||
if (mode->flags & DRM_MODE_FLAG_DBLSCAN) {
|
||||
mhz /= 2;
|
||||
}
|
||||
if (mode->vscan > 1) {
|
||||
mhz /= mode->vscan;
|
||||
}
|
||||
return (float)mhz / 1000;
|
||||
}
|
||||
97
src/libs/drm/drm.h
Normal file
97
src/libs/drm/drm.h
Normal file
@@ -0,0 +1,97 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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 <xf86drmMode.h>
|
||||
|
||||
#include "../types.h"
|
||||
#include "../frame.h"
|
||||
#include "../frametext.h"
|
||||
#include "../capture.h"
|
||||
|
||||
|
||||
typedef enum {
|
||||
US_DRM_STUB_USER = 1,
|
||||
US_DRM_STUB_BAD_RESOLUTION,
|
||||
US_DRM_STUB_BAD_FORMAT,
|
||||
US_DRM_STUB_NO_SIGNAL,
|
||||
US_DRM_STUB_BUSY,
|
||||
} us_drm_stub_e;
|
||||
|
||||
typedef struct {
|
||||
u32 id;
|
||||
u32 handle;
|
||||
u8 *data;
|
||||
uz allocated;
|
||||
bool dumb_created;
|
||||
bool fb_added;
|
||||
struct {
|
||||
bool *has_vsync;
|
||||
int *exposing_dma_fd;
|
||||
} ctx;
|
||||
} us_drm_buffer_s;
|
||||
|
||||
typedef struct {
|
||||
int status_fd;
|
||||
int fd;
|
||||
u32 crtc_id;
|
||||
u32 conn_id;
|
||||
u32 dpms_id;
|
||||
drmModeModeInfo mode;
|
||||
us_drm_buffer_s *bufs;
|
||||
uint n_bufs;
|
||||
drmModeCrtc *saved_crtc;
|
||||
int dpms_state;
|
||||
int opened;
|
||||
|
||||
bool has_vsync;
|
||||
int exposing_dma_fd;
|
||||
uint stub_n_buf;
|
||||
ldf blank_at_ts;
|
||||
|
||||
int once;
|
||||
us_frametext_s *ft;
|
||||
} us_drm_runtime_s;
|
||||
|
||||
typedef struct {
|
||||
char *path;
|
||||
char *port;
|
||||
uint timeout;
|
||||
uint blank_after;
|
||||
|
||||
us_drm_runtime_s *run;
|
||||
} us_drm_s;
|
||||
|
||||
|
||||
us_drm_s *us_drm_init(void);
|
||||
void us_drm_destroy(us_drm_s *drm);
|
||||
|
||||
int us_drm_open(us_drm_s *drm, const us_capture_s *cap);
|
||||
void us_drm_close(us_drm_s *drm);
|
||||
|
||||
int us_drm_dpms_power_off(us_drm_s *drm);
|
||||
int us_drm_wait_for_vsync(us_drm_s *drm);
|
||||
int us_drm_expose_stub(us_drm_s *drm, us_drm_stub_e stub, const us_capture_s *cap);
|
||||
int us_drm_expose_dma(us_drm_s *drm, const us_capture_hwbuf_s *hw);
|
||||
int us_drm_ensure_no_signal(us_drm_s *drm);
|
||||
31
src/libs/errors.h
Normal file
31
src/libs/errors.h
Normal file
@@ -0,0 +1,31 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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
|
||||
|
||||
#define US_ERROR_COMMON -1
|
||||
#define US_ERROR_NO_DEVICE -2
|
||||
#define US_ERROR_NO_CABLE -3
|
||||
#define US_ERROR_NO_SIGNAL -4
|
||||
#define US_ERROR_NO_SYNC -5
|
||||
#define US_ERROR_NO_LANES -6
|
||||
#define US_ERROR_NO_DATA -7
|
||||
109
src/libs/fpsi.c
Normal file
109
src/libs/fpsi.c
Normal file
@@ -0,0 +1,109 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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 "fpsi.h"
|
||||
|
||||
#include <stdatomic.h>
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
#include "types.h"
|
||||
#include "tools.h"
|
||||
#include "threading.h"
|
||||
#include "logging.h"
|
||||
#include "frame.h"
|
||||
|
||||
|
||||
us_fpsi_s *us_fpsi_init(const char *name, bool with_meta) {
|
||||
us_fpsi_s *fpsi;
|
||||
US_CALLOC(fpsi, 1);
|
||||
fpsi->name = us_strdup(name);
|
||||
fpsi->with_meta = with_meta;
|
||||
atomic_init(&fpsi->state_ts, 0);
|
||||
atomic_init(&fpsi->state, 0);
|
||||
return fpsi;
|
||||
}
|
||||
|
||||
void us_fpsi_destroy(us_fpsi_s *fpsi) {
|
||||
free(fpsi->name);
|
||||
free(fpsi);
|
||||
}
|
||||
|
||||
void us_fpsi_frame_to_meta(const us_frame_s *frame, us_fpsi_meta_s *meta) {
|
||||
meta->width = frame->width;
|
||||
meta->height = frame->height;
|
||||
meta->online = frame->online;
|
||||
}
|
||||
|
||||
void us_fpsi_update(us_fpsi_s *fpsi, bool bump, const us_fpsi_meta_s *meta) {
|
||||
if (meta != NULL) {
|
||||
US_A(fpsi->with_meta);
|
||||
} else {
|
||||
US_A(!fpsi->with_meta);
|
||||
}
|
||||
|
||||
const sll now_ts = us_floor_ms(us_get_now_monotonic());
|
||||
if (atomic_load(&fpsi->state_ts) != now_ts) {
|
||||
US_LOG_PERF_FPS("FPS: %s: %u", fpsi->name, fpsi->accum);
|
||||
|
||||
// Fast mutex-less store method
|
||||
ull state = (ull)fpsi->accum & 0xFFFF;
|
||||
if (fpsi->with_meta) {
|
||||
US_A(meta != NULL);
|
||||
state |= (ull)(meta->width & 0xFFFF) << 16;
|
||||
state |= (ull)(meta->height & 0xFFFF) << 32;
|
||||
state |= (ull)(meta->online ? 1 : 0) << 48;
|
||||
}
|
||||
atomic_store(&fpsi->state, state); // Сначала инфа
|
||||
atomic_store(&fpsi->state_ts, now_ts); // Потом время, это важно
|
||||
fpsi->accum = 0;
|
||||
}
|
||||
if (bump) {
|
||||
++fpsi->accum;
|
||||
}
|
||||
}
|
||||
|
||||
uint us_fpsi_get(us_fpsi_s *fpsi, us_fpsi_meta_s *meta) {
|
||||
if (meta != NULL) {
|
||||
US_A(fpsi->with_meta);
|
||||
}
|
||||
|
||||
// Между чтением инфы и времени может быть гонка,
|
||||
// но это неважно. Если время свежее, до данные тоже
|
||||
// будут свежмими, обратный случай не так важен.
|
||||
const sll now_ts = us_floor_ms(us_get_now_monotonic());
|
||||
const sll state_ts = atomic_load(&fpsi->state_ts); // Сначала время
|
||||
const ull state = atomic_load(&fpsi->state); // Потом инфа
|
||||
|
||||
uint current = state & 0xFFFF;
|
||||
if (fpsi->with_meta && meta != NULL) {
|
||||
meta->width = (state >> 16) & 0xFFFF;
|
||||
meta->height = (state >> 32) & 0xFFFF;
|
||||
meta->online = (state >> 48) & 1;
|
||||
}
|
||||
|
||||
if (state_ts != now_ts && (state_ts + 1) != now_ts) {
|
||||
// Только текущая или прошлая секунда
|
||||
current = 0;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,18 +22,30 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#include "uslibs/tools.h"
|
||||
#include "uslibs/frame.h"
|
||||
#include "uslibs/memsinksh.h"
|
||||
|
||||
#include "logging.h"
|
||||
#include "types.h"
|
||||
#include "frame.h"
|
||||
|
||||
|
||||
int us_memsink_fd_wait_frame(int fd, us_memsink_shared_s* mem, uint64_t last_id);
|
||||
us_frame_s *us_memsink_fd_get_frame(int fd, us_memsink_shared_s *mem, uint64_t *frame_id, bool key_required);
|
||||
typedef struct {
|
||||
uint width;
|
||||
uint height;
|
||||
bool online;
|
||||
} us_fpsi_meta_s;
|
||||
|
||||
typedef struct {
|
||||
char *name;
|
||||
bool with_meta;
|
||||
uint accum;
|
||||
atomic_llong state_ts;
|
||||
atomic_ullong state;
|
||||
} us_fpsi_s;
|
||||
|
||||
|
||||
us_fpsi_s *us_fpsi_init(const char *name, bool with_meta);
|
||||
void us_fpsi_destroy(us_fpsi_s *fpsi);
|
||||
|
||||
void us_fpsi_frame_to_meta(const us_frame_s *frame, us_fpsi_meta_s *meta);
|
||||
void us_fpsi_update(us_fpsi_s *fpsi, bool bump, const us_fpsi_meta_s *meta);
|
||||
uint us_fpsi_get(us_fpsi_s *fpsi, us_fpsi_meta_s *meta);
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,11 +22,20 @@
|
||||
|
||||
#include "frame.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#include "types.h"
|
||||
#include "tools.h"
|
||||
|
||||
|
||||
us_frame_s *us_frame_init(void) {
|
||||
us_frame_s *frame;
|
||||
US_CALLOC(frame, 1);
|
||||
us_frame_realloc_data(frame, 512 * 1024);
|
||||
us_frame_realloc_data(frame, 32 * 1024);
|
||||
frame->dma_fd = -1;
|
||||
return frame;
|
||||
}
|
||||
@@ -36,21 +45,21 @@ void us_frame_destroy(us_frame_s *frame) {
|
||||
free(frame);
|
||||
}
|
||||
|
||||
void us_frame_realloc_data(us_frame_s *frame, size_t size) {
|
||||
void us_frame_realloc_data(us_frame_s *frame, uz size) {
|
||||
if (frame->allocated < size) {
|
||||
US_REALLOC(frame->data, size);
|
||||
frame->allocated = size;
|
||||
}
|
||||
}
|
||||
|
||||
void us_frame_set_data(us_frame_s *frame, const uint8_t *data, size_t size) {
|
||||
void us_frame_set_data(us_frame_s *frame, const u8 *data, uz size) {
|
||||
us_frame_realloc_data(frame, size);
|
||||
memcpy(frame->data, data, size);
|
||||
frame->used = size;
|
||||
}
|
||||
|
||||
void us_frame_append_data(us_frame_s *frame, const uint8_t *data, size_t size) {
|
||||
const size_t new_used = frame->used + size;
|
||||
void us_frame_append_data(us_frame_s *frame, const u8 *data, uz size) {
|
||||
const uz new_used = frame->used + size;
|
||||
us_frame_realloc_data(frame, new_used);
|
||||
memcpy(frame->data + frame->used, data, size);
|
||||
frame->used = new_used;
|
||||
@@ -64,23 +73,40 @@ void us_frame_copy(const us_frame_s *src, us_frame_s *dest) {
|
||||
bool us_frame_compare(const us_frame_s *a, const us_frame_s *b) {
|
||||
return (
|
||||
a->allocated && b->allocated
|
||||
&& US_FRAME_COMPARE_META_USED_NOTS(a, b)
|
||||
&& US_FRAME_COMPARE_GEOMETRY(a, b)
|
||||
&& !memcmp(a->data, b->data, b->used)
|
||||
);
|
||||
}
|
||||
|
||||
unsigned us_frame_get_padding(const us_frame_s *frame) {
|
||||
unsigned bytes_per_pixel = 0;
|
||||
uint us_frame_get_padding(const us_frame_s *frame) {
|
||||
uint bytes_per_pixel = 0;
|
||||
switch (frame->format) {
|
||||
case V4L2_PIX_FMT_YUV420:
|
||||
case V4L2_PIX_FMT_YVU420:
|
||||
case V4L2_PIX_FMT_GREY:
|
||||
bytes_per_pixel = 1;
|
||||
break;
|
||||
|
||||
case V4L2_PIX_FMT_YUYV:
|
||||
case V4L2_PIX_FMT_YVYU:
|
||||
case V4L2_PIX_FMT_UYVY:
|
||||
case V4L2_PIX_FMT_RGB565: bytes_per_pixel = 2; break;
|
||||
case V4L2_PIX_FMT_RGB565:
|
||||
bytes_per_pixel = 2;
|
||||
break;
|
||||
|
||||
case V4L2_PIX_FMT_BGR24:
|
||||
case V4L2_PIX_FMT_RGB24: bytes_per_pixel = 3; break;
|
||||
case V4L2_PIX_FMT_RGB24:
|
||||
bytes_per_pixel = 3;
|
||||
break;
|
||||
|
||||
// case V4L2_PIX_FMT_H264:
|
||||
case V4L2_PIX_FMT_MJPEG:
|
||||
case V4L2_PIX_FMT_JPEG: bytes_per_pixel = 0; break;
|
||||
default: assert(0 && "Unknown format");
|
||||
case V4L2_PIX_FMT_JPEG:
|
||||
bytes_per_pixel = 0;
|
||||
break;
|
||||
|
||||
default:
|
||||
US_RAISE("Unknown format");
|
||||
}
|
||||
if (bytes_per_pixel > 0 && frame->stride > frame->width) {
|
||||
return (frame->stride - frame->width * bytes_per_pixel);
|
||||
@@ -88,13 +114,17 @@ unsigned us_frame_get_padding(const us_frame_s *frame) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *us_fourcc_to_string(unsigned format, char *buf, size_t size) {
|
||||
assert(size >= 8);
|
||||
bool us_is_jpeg(uint format) {
|
||||
return (format == V4L2_PIX_FMT_JPEG || format == V4L2_PIX_FMT_MJPEG);
|
||||
}
|
||||
|
||||
const char *us_fourcc_to_string(uint format, char *buf, uz size) {
|
||||
US_A(size >= 8);
|
||||
buf[0] = format & 0x7F;
|
||||
buf[1] = (format >> 8) & 0x7F;
|
||||
buf[2] = (format >> 16) & 0x7F;
|
||||
buf[3] = (format >> 24) & 0x7F;
|
||||
if (format & ((unsigned)1 << 31)) {
|
||||
if (format & ((uint)1 << 31)) {
|
||||
buf[4] = '-';
|
||||
buf[5] = 'B';
|
||||
buf[6] = 'E';
|
||||
|
||||
123
src/libs/frame.h
123
src/libs/frame.h
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,74 +22,70 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#include "types.h"
|
||||
#include "tools.h"
|
||||
|
||||
|
||||
#define US_FRAME_META_DECLARE \
|
||||
uint width; \
|
||||
uint height; \
|
||||
uint format; \
|
||||
uint stride; \
|
||||
/* Stride is a bytesperline in V4L2 */ \
|
||||
/* https://www.kernel.org/doc/html/v4.14/media/uapi/v4l/pixfmt-v4l2.html */ \
|
||||
/* https://medium.com/@oleg.shipitko/what-does-stride-mean-in-image-processing-bba158a72bcd */ \
|
||||
bool online; \
|
||||
bool key; \
|
||||
uint gop; \
|
||||
\
|
||||
ldf grab_begin_ts; \
|
||||
ldf grab_end_ts; \
|
||||
ldf encode_begin_ts; \
|
||||
ldf encode_end_ts;
|
||||
|
||||
|
||||
typedef struct {
|
||||
uint8_t *data;
|
||||
size_t used;
|
||||
size_t allocated;
|
||||
int dma_fd;
|
||||
u8 *data;
|
||||
uz used;
|
||||
uz allocated;
|
||||
int dma_fd;
|
||||
|
||||
unsigned width;
|
||||
unsigned height;
|
||||
unsigned format;
|
||||
unsigned stride;
|
||||
// Stride is a bytesperline in V4L2
|
||||
// https://www.kernel.org/doc/html/v4.14/media/uapi/v4l/pixfmt-v4l2.html
|
||||
// https://medium.com/@oleg.shipitko/what-does-stride-mean-in-image-processing-bba158a72bcd
|
||||
|
||||
bool online;
|
||||
bool key;
|
||||
unsigned gop;
|
||||
|
||||
long double grab_ts;
|
||||
long double encode_begin_ts;
|
||||
long double encode_end_ts;
|
||||
US_FRAME_META_DECLARE;
|
||||
} us_frame_s;
|
||||
|
||||
|
||||
#define US_FRAME_COPY_META(x_src, x_dest) { \
|
||||
x_dest->width = x_src->width; \
|
||||
x_dest->height = x_src->height; \
|
||||
x_dest->format = x_src->format; \
|
||||
x_dest->stride = x_src->stride; \
|
||||
x_dest->online = x_src->online; \
|
||||
x_dest->key = x_src->key; \
|
||||
x_dest->gop = x_src->gop; \
|
||||
x_dest->grab_ts = x_src->grab_ts; \
|
||||
x_dest->encode_begin_ts = x_src->encode_begin_ts; \
|
||||
x_dest->encode_end_ts = x_src->encode_end_ts; \
|
||||
(x_dest)->width = (x_src)->width; \
|
||||
(x_dest)->height = (x_src)->height; \
|
||||
(x_dest)->format = (x_src)->format; \
|
||||
(x_dest)->stride = (x_src)->stride; \
|
||||
(x_dest)->online = (x_src)->online; \
|
||||
(x_dest)->key = (x_src)->key; \
|
||||
(x_dest)->gop = (x_src)->gop; \
|
||||
\
|
||||
(x_dest)->grab_begin_ts = (x_src)->grab_begin_ts; \
|
||||
(x_dest)->grab_end_ts = (x_src)->grab_end_ts; \
|
||||
(x_dest)->encode_begin_ts = (x_src)->encode_begin_ts; \
|
||||
(x_dest)->encode_end_ts = (x_src)->encode_end_ts; \
|
||||
}
|
||||
|
||||
static inline void us_frame_copy_meta(const us_frame_s *src, us_frame_s *dest) {
|
||||
US_FRAME_COPY_META(src, dest);
|
||||
}
|
||||
|
||||
#define US_FRAME_COMPARE_META_USED_NOTS(x_a, x_b) ( \
|
||||
x_a->used == x_b->used \
|
||||
&& x_a->width == x_b->width \
|
||||
&& x_a->height == x_b->height \
|
||||
&& x_a->format == x_b->format \
|
||||
&& x_a->stride == x_b->stride \
|
||||
&& x_a->online == x_b->online \
|
||||
&& x_a->key == x_b->key \
|
||||
&& x_a->gop == x_b->gop \
|
||||
#define US_FRAME_COMPARE_GEOMETRY(x_a, x_b) ( \
|
||||
/* Compare the used size and significant meta (no timings) */ \
|
||||
(x_a)->used == (x_b)->used \
|
||||
\
|
||||
&& (x_a)->width == (x_b)->width \
|
||||
&& (x_a)->height == (x_b)->height \
|
||||
&& (x_a)->format == (x_b)->format \
|
||||
&& (x_a)->stride == (x_b)->stride \
|
||||
&& (x_a)->online == (x_b)->online \
|
||||
&& (x_a)->key == (x_b)->key \
|
||||
&& (x_a)->gop == (x_b)->gop \
|
||||
)
|
||||
|
||||
|
||||
static inline void us_frame_encoding_begin(const us_frame_s *src, us_frame_s *dest, unsigned format) {
|
||||
assert(src->used > 0);
|
||||
us_frame_copy_meta(src, dest);
|
||||
static inline void us_frame_encoding_begin(const us_frame_s *src, us_frame_s *dest, uint format) {
|
||||
US_A(src->used > 0);
|
||||
US_FRAME_COPY_META(src, dest);
|
||||
dest->encode_begin_ts = us_get_now_monotonic();
|
||||
dest->format = format;
|
||||
dest->stride = 0;
|
||||
@@ -97,7 +93,7 @@ static inline void us_frame_encoding_begin(const us_frame_s *src, us_frame_s *de
|
||||
}
|
||||
|
||||
static inline void us_frame_encoding_end(us_frame_s *dest) {
|
||||
assert(dest->used > 0);
|
||||
US_A(dest->used > 0);
|
||||
dest->encode_end_ts = us_get_now_monotonic();
|
||||
}
|
||||
|
||||
@@ -105,17 +101,14 @@ static inline void us_frame_encoding_end(us_frame_s *dest) {
|
||||
us_frame_s *us_frame_init(void);
|
||||
void us_frame_destroy(us_frame_s *frame);
|
||||
|
||||
void us_frame_realloc_data(us_frame_s *frame, size_t size);
|
||||
void us_frame_set_data(us_frame_s *frame, const uint8_t *data, size_t size);
|
||||
void us_frame_append_data(us_frame_s *frame, const uint8_t *data, size_t size);
|
||||
void us_frame_realloc_data(us_frame_s *frame, uz size);
|
||||
void us_frame_set_data(us_frame_s *frame, const u8 *data, uz size);
|
||||
void us_frame_append_data(us_frame_s *frame, const u8 *data, uz size);
|
||||
|
||||
void us_frame_copy(const us_frame_s *src, us_frame_s *dest);
|
||||
bool us_frame_compare(const us_frame_s *a, const us_frame_s *b);
|
||||
|
||||
unsigned us_frame_get_padding(const us_frame_s *frame);
|
||||
uint us_frame_get_padding(const us_frame_s *frame);
|
||||
|
||||
const char *us_fourcc_to_string(unsigned format, char *buf, size_t size);
|
||||
|
||||
static inline bool us_is_jpeg(unsigned format) {
|
||||
return (format == V4L2_PIX_FMT_JPEG || format == V4L2_PIX_FMT_MJPEG);
|
||||
}
|
||||
bool us_is_jpeg(uint format);
|
||||
const char *us_fourcc_to_string(uint format, char *buf, uz size);
|
||||
|
||||
199
src/libs/frametext.c
Normal file
199
src/libs/frametext.c
Normal file
@@ -0,0 +1,199 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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 "frametext.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#include "tools.h"
|
||||
#include "frame.h"
|
||||
#include "frametext_font.h"
|
||||
|
||||
|
||||
static void _frametext_draw_line(
|
||||
us_frametext_s *ft,
|
||||
const char *line,
|
||||
uint scale_x,
|
||||
uint scale_y,
|
||||
uint start_x,
|
||||
uint start_y);
|
||||
|
||||
|
||||
us_frametext_s *us_frametext_init(void) {
|
||||
us_frametext_s *ft;
|
||||
US_CALLOC(ft, 1);
|
||||
ft->frame = us_frame_init();
|
||||
return ft;
|
||||
}
|
||||
|
||||
void us_frametext_destroy(us_frametext_s *ft) {
|
||||
us_frame_destroy(ft->frame);
|
||||
US_DELETE(ft->text, free);
|
||||
free(ft);
|
||||
}
|
||||
|
||||
/*
|
||||
Every character in the font is encoded row-wise in 8 bytes.
|
||||
The least significant bit of each byte corresponds to the first pixel in a row.
|
||||
The character 'A' (0x41 / 65) is encoded as { 0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00}
|
||||
|
||||
0x0C => 0000 1100 => ..XX....
|
||||
0X1E => 0001 1110 => .XXXX...
|
||||
0x33 => 0011 0011 => XX..XX..
|
||||
0x33 => 0011 0011 => XX..XX..
|
||||
0x3F => 0011 1111 => xxxxxx..
|
||||
0x33 => 0011 0011 => XX..XX..
|
||||
0x33 => 0011 0011 => XX..XX..
|
||||
0x00 => 0000 0000 => ........
|
||||
|
||||
To access the nth pixel in a row, right-shift by n.
|
||||
|
||||
. . X X . . . .
|
||||
| | | | | | | |
|
||||
(0x0C >> 0) & 1 == 0-+ | | | | | | |
|
||||
(0x0C >> 1) & 1 == 0---+ | | | | | |
|
||||
(0x0C >> 2) & 1 == 1-----+ | | | | |
|
||||
(0x0C >> 3) & 1 == 1-------+ | | | |
|
||||
(0x0C >> 4) & 1 == 0---------+ | | |
|
||||
(0x0C >> 5) & 1 == 0-----------+ | |
|
||||
(0x0C >> 6) & 1 == 0-------------+ |
|
||||
(0x0C >> 7) & 1 == 0---------------+
|
||||
*/
|
||||
|
||||
void us_frametext_draw(us_frametext_s *ft, const char *text, uint width, uint height) {
|
||||
US_A(width > 0);
|
||||
US_A(height > 0);
|
||||
|
||||
us_frame_s *const frame = ft->frame;
|
||||
|
||||
if (
|
||||
frame->width == width && frame->height == height
|
||||
&& ft->text != NULL && !strcmp(ft->text, text)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
US_DELETE(ft->text, free);
|
||||
ft->text = us_strdup(text);
|
||||
strcpy(ft->text, text);
|
||||
frame->width = width;
|
||||
frame->height = height;
|
||||
frame->format = V4L2_PIX_FMT_RGB24;
|
||||
frame->stride = width * 3;
|
||||
frame->used = width * height * 3;
|
||||
us_frame_realloc_data(frame, frame->used);
|
||||
memset(frame->data, 0, frame->used);
|
||||
|
||||
if (frame->width == 0 || frame->height == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
char *str = us_strdup(text);
|
||||
const char *line;
|
||||
char *rest;
|
||||
|
||||
uint block_width = 0;
|
||||
uint block_height = 0;
|
||||
while ((line = strtok_r((block_height == 0 ? str : NULL), "\n", &rest)) != NULL) {
|
||||
block_width = US_MAX(strlen(line) * 8, block_width);
|
||||
block_height += 8;
|
||||
}
|
||||
if (block_width == 0 || block_height == 0) {
|
||||
goto empty;
|
||||
}
|
||||
|
||||
// Ширина текста должна быть от 75%, до половины экрана, в зависимости от длины
|
||||
const float div_x = US_MAX(US_MIN((100 / block_width * 2), 2.0), 1.5);
|
||||
|
||||
// Высоту тоже отрегулировать как-нибудь
|
||||
const float div_y = US_MAX(US_MIN((70 / block_height * 2), 2.0), 1.5);
|
||||
|
||||
uint scale_x = frame->width / block_width / div_x;
|
||||
uint scale_y = frame->height / block_height / div_y;
|
||||
if (scale_x < scale_y / 1.5) { // Keep proportions
|
||||
scale_y = scale_x * 1.5;
|
||||
} else if (scale_y < scale_x * 1.5) {
|
||||
scale_x = scale_y / 1.5;
|
||||
}
|
||||
|
||||
strcpy(str, text);
|
||||
|
||||
const uint start_y = (frame->height >= (block_height * scale_y)
|
||||
? ((frame->height - (block_height * scale_y)) / 2)
|
||||
: 0);
|
||||
uint n_line = 0;
|
||||
while ((line = strtok_r((n_line == 0 ? str : NULL), "\n", &rest)) != NULL) {
|
||||
const uint line_width = strlen(line) * 8 * scale_x;
|
||||
const uint start_x = (frame->width >= line_width
|
||||
? ((frame->width - line_width) / 2)
|
||||
: 0);
|
||||
_frametext_draw_line(ft, line, scale_x, scale_y, start_x, start_y + n_line * 8 * scale_y);
|
||||
++n_line;
|
||||
}
|
||||
|
||||
empty:
|
||||
free(str);
|
||||
}
|
||||
|
||||
void _frametext_draw_line(
|
||||
us_frametext_s *ft,
|
||||
const char *line,
|
||||
uint scale_x,
|
||||
uint scale_y,
|
||||
uint start_x,
|
||||
uint start_y
|
||||
) {
|
||||
us_frame_s *const frame = ft->frame;
|
||||
|
||||
const size_t len = strlen(line);
|
||||
|
||||
for (uint ch_y = 0; ch_y < 8 * scale_y; ++ch_y) {
|
||||
const uint canvas_y = start_y + ch_y;
|
||||
for (uint ch_x = 0; ch_x < 8 * len * scale_x; ++ch_x) {
|
||||
if ((start_x + ch_x) >= frame->width) {
|
||||
break;
|
||||
}
|
||||
const uint canvas_x = (start_x + ch_x) * 3;
|
||||
const uint offset = canvas_y * frame->stride + canvas_x;
|
||||
if (offset >= frame->used) {
|
||||
break;
|
||||
}
|
||||
|
||||
const u8 ch = US_MIN((u8)line[ch_x / 8 / scale_x], sizeof(US_FRAMETEXT_FONT) / 8 - 1);
|
||||
const uint ch_byte = (ch_y / scale_y) % 8;
|
||||
const uint ch_bit = (ch_x / scale_x) % 8;
|
||||
const bool pix_on = !!(US_FRAMETEXT_FONT[ch][ch_byte] & (1 << ch_bit));
|
||||
|
||||
u8 *const r = &frame->data[offset];
|
||||
u8 *const g = r + 1;
|
||||
u8 *const b = r + 2;
|
||||
|
||||
*r = pix_on * 0x65; // RGB/BGR-friendly
|
||||
*g = pix_on * 0x65;
|
||||
*b = pix_on * 0x65;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/libs/frametext.h
Normal file
39
src/libs/frametext.h
Normal file
@@ -0,0 +1,39 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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 "types.h"
|
||||
#include "frame.h"
|
||||
|
||||
|
||||
typedef struct {
|
||||
char *text;
|
||||
us_frame_s *frame;
|
||||
} us_frametext_s;
|
||||
|
||||
|
||||
us_frametext_s *us_frametext_init(void);
|
||||
void us_frametext_destroy(us_frametext_s *ft);
|
||||
|
||||
void us_frametext_draw(us_frametext_s *ft, const char *text, uint width, uint height);
|
||||
160
src/libs/frametext_font.c
Normal file
160
src/libs/frametext_font.c
Normal file
@@ -0,0 +1,160 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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 "frametext_font.h"
|
||||
|
||||
#include "types.h"
|
||||
|
||||
|
||||
const u8 US_FRAMETEXT_FONT[128][8] = {
|
||||
// https://github.com/dhepper/font8x8/blob/master/font8x8_basic.h
|
||||
// Author: Daniel Hepper <daniel@hepper.net>
|
||||
// License: Public Domain
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0000 (nul)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0001
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0002
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0003
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0004
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0005
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0006
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0007
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0008
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0009
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000A
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000B
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000C
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000D
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000E
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000F
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0010
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0011
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0012
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0013
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0014
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0015
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0016
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0017
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0018
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0019
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001A
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001B
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001C
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001D
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001E
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001F
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0020 (space)
|
||||
{0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00}, // U+0021 (!)
|
||||
{0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0022 (")
|
||||
{0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00}, // U+0023 (#)
|
||||
{0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00}, // U+0024 ($)
|
||||
{0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00}, // U+0025 (%)
|
||||
{0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00}, // U+0026 (&)
|
||||
{0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0027 (')
|
||||
{0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00}, // U+0028 (()
|
||||
{0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00}, // U+0029 ())
|
||||
{0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00}, // U+002A (*)
|
||||
{0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00}, // U+002B (+)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // U+002C (,)
|
||||
{0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00}, // U+002D (-)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // U+002E (.)
|
||||
{0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00}, // U+002F (/)
|
||||
{0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00}, // U+0030 (0)
|
||||
{0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00}, // U+0031 (1)
|
||||
{0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00}, // U+0032 (2)
|
||||
{0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00}, // U+0033 (3)
|
||||
{0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00}, // U+0034 (4)
|
||||
{0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00}, // U+0035 (5)
|
||||
{0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00}, // U+0036 (6)
|
||||
{0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00}, // U+0037 (7)
|
||||
{0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00}, // U+0038 (8)
|
||||
{0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00}, // U+0039 (9)
|
||||
{0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // U+003A (:)
|
||||
{0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // U+003B (;)
|
||||
{0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00}, // U+003C (<)
|
||||
{0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00}, // U+003D (=)
|
||||
{0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00}, // U+003E (>)
|
||||
{0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00}, // U+003F (?)
|
||||
{0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00}, // U+0040 (@)
|
||||
{0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00}, // U+0041 (A)
|
||||
{0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00}, // U+0042 (B)
|
||||
{0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00}, // U+0043 (C)
|
||||
{0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00}, // U+0044 (D)
|
||||
{0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00}, // U+0045 (E)
|
||||
{0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00}, // U+0046 (F)
|
||||
{0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00}, // U+0047 (G)
|
||||
{0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00}, // U+0048 (H)
|
||||
{0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0049 (I)
|
||||
{0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00}, // U+004A (J)
|
||||
{0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00}, // U+004B (K)
|
||||
{0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00}, // U+004C (L)
|
||||
{0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00}, // U+004D (M)
|
||||
{0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00}, // U+004E (N)
|
||||
{0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00}, // U+004F (O)
|
||||
{0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00}, // U+0050 (P)
|
||||
{0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00}, // U+0051 (Q)
|
||||
{0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00}, // U+0052 (R)
|
||||
{0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00}, // U+0053 (S)
|
||||
{0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0054 (T)
|
||||
{0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00}, // U+0055 (U)
|
||||
{0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // U+0056 (V)
|
||||
{0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00}, // U+0057 (W)
|
||||
{0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00}, // U+0058 (X)
|
||||
{0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00}, // U+0059 (Y)
|
||||
{0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00}, // U+005A (Z)
|
||||
{0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00}, // U+005B ([)
|
||||
{0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00}, // U+005C (\)
|
||||
{0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00}, // U+005D (])
|
||||
{0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00}, // U+005E (^)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF}, // U+005F (_)
|
||||
{0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0060 (`)
|
||||
{0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00}, // U+0061 (a)
|
||||
{0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00}, // U+0062 (b)
|
||||
{0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00}, // U+0063 (c)
|
||||
{0x38, 0x30, 0x30, 0x3e, 0x33, 0x33, 0x6E, 0x00}, // U+0064 (d)
|
||||
{0x00, 0x00, 0x1E, 0x33, 0x3f, 0x03, 0x1E, 0x00}, // U+0065 (e)
|
||||
{0x1C, 0x36, 0x06, 0x0f, 0x06, 0x06, 0x0F, 0x00}, // U+0066 (f)
|
||||
{0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // U+0067 (g)
|
||||
{0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00}, // U+0068 (h)
|
||||
{0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0069 (i)
|
||||
{0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E}, // U+006A (j)
|
||||
{0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00}, // U+006B (k)
|
||||
{0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+006C (l)
|
||||
{0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00}, // U+006D (m)
|
||||
{0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00}, // U+006E (n)
|
||||
{0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00}, // U+006F (o)
|
||||
{0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F}, // U+0070 (p)
|
||||
{0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78}, // U+0071 (q)
|
||||
{0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00}, // U+0072 (r)
|
||||
{0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00}, // U+0073 (s)
|
||||
{0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00}, // U+0074 (t)
|
||||
{0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00}, // U+0075 (u)
|
||||
{0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // U+0076 (v)
|
||||
{0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00}, // U+0077 (w)
|
||||
{0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00}, // U+0078 (x)
|
||||
{0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // U+0079 (y)
|
||||
{0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00}, // U+007A (z)
|
||||
{0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00}, // U+007B ({)
|
||||
{0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00}, // U+007C (|)
|
||||
{0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00}, // U+007D (})
|
||||
{0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+007E (~)
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+007F
|
||||
};
|
||||
28
src/libs/frametext_font.h
Normal file
28
src/libs/frametext_font.h
Normal file
@@ -0,0 +1,28 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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 "types.h"
|
||||
|
||||
|
||||
extern const u8 US_FRAMETEXT_FONT[128][8];
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,12 +22,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <assert.h>
|
||||
#include "tools.h"
|
||||
|
||||
|
||||
#define US_LIST_STRUCT(...) \
|
||||
__VA_ARGS__ *prev; \
|
||||
__VA_ARGS__ *next;
|
||||
#define US_LIST_DECLARE \
|
||||
void *prev; \
|
||||
void *next;
|
||||
|
||||
#define US_LIST_ITERATE(x_first, x_item, ...) { \
|
||||
for (__typeof__(x_first) x_item = x_first; x_item;) { \
|
||||
@@ -42,10 +42,11 @@
|
||||
x_first = x_item; \
|
||||
} else { \
|
||||
__typeof__(x_first) m_last = x_first; \
|
||||
for (; m_last->next; m_last = m_last->next); \
|
||||
for (; m_last->next != NULL; m_last = m_last->next); \
|
||||
x_item->prev = m_last; \
|
||||
m_last->next = x_item; \
|
||||
} \
|
||||
x_item->next = NULL; \
|
||||
}
|
||||
|
||||
#define US_LIST_APPEND_C(x_first, x_item, x_count) { \
|
||||
@@ -57,15 +58,19 @@
|
||||
if (x_item->prev == NULL) { \
|
||||
x_first = x_item->next; \
|
||||
} else { \
|
||||
x_item->prev->next = x_item->next; \
|
||||
__typeof__(x_first) m_prev = x_item->prev; \
|
||||
m_prev->next = x_item->next; \
|
||||
} \
|
||||
if (x_item->next != NULL) { \
|
||||
x_item->next->prev = x_item->prev; \
|
||||
__typeof__(x_first) m_next = x_item->next; \
|
||||
m_next->prev = x_item->prev; \
|
||||
} \
|
||||
x_item->prev = NULL; \
|
||||
x_item->next = NULL; \
|
||||
}
|
||||
|
||||
#define US_LIST_REMOVE_C(x_first, x_item, x_count) { \
|
||||
US_LIST_REMOVE(x_first, x_item); \
|
||||
assert((x_count) >= 1); \
|
||||
US_A((x_count) >= 1); \
|
||||
--(x_count); \
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,6 +22,10 @@
|
||||
|
||||
#include "logging.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
|
||||
enum us_log_level_t us_g_log_level;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -23,18 +23,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
#include "types.h"
|
||||
#include "tools.h"
|
||||
#include "threading.h"
|
||||
#include "logging_base.h"
|
||||
|
||||
|
||||
enum us_log_level_t {
|
||||
@@ -73,37 +73,7 @@ extern pthread_mutex_t us_g_log_mutex;
|
||||
#define US_COLOR_RESET "\x1b[0m"
|
||||
|
||||
|
||||
#define US_SEP_INFO(x_ch) { \
|
||||
US_LOGGING_LOCK; \
|
||||
for (int m_count = 0; m_count < 80; ++m_count) { \
|
||||
fputc((x_ch), stderr); \
|
||||
} \
|
||||
fputc('\n', stderr); \
|
||||
fflush(stderr); \
|
||||
US_LOGGING_UNLOCK; \
|
||||
}
|
||||
|
||||
#define US_SEP_DEBUG(x_ch) { \
|
||||
if (us_g_log_level >= US_LOG_LEVEL_DEBUG) { \
|
||||
US_SEP_INFO(x_ch); \
|
||||
} \
|
||||
}
|
||||
|
||||
|
||||
#define US_LOG_PRINTF_NOLOCK(x_label_color, x_label, x_msg_color, x_msg, ...) { \
|
||||
char m_tname_buf[US_MAX_THREAD_NAME] = {0}; \
|
||||
us_thread_get_name(m_tname_buf); \
|
||||
if (us_g_log_colored) { \
|
||||
fprintf(stderr, US_COLOR_GRAY "-- " x_label_color x_label US_COLOR_GRAY \
|
||||
" [%.03Lf %9s]" " -- " US_COLOR_RESET x_msg_color x_msg US_COLOR_RESET, \
|
||||
us_get_now_monotonic(), m_tname_buf, ##__VA_ARGS__); \
|
||||
} else { \
|
||||
fprintf(stderr, "-- " x_label " [%.03Lf %9s] -- " x_msg, \
|
||||
us_get_now_monotonic(), m_tname_buf, ##__VA_ARGS__); \
|
||||
} \
|
||||
fputc('\n', stderr); \
|
||||
fflush(stderr); \
|
||||
}
|
||||
// XXX: See the definition of US_LOG_PRINTF_NOLOCK() in logging_base.h
|
||||
|
||||
#define US_LOG_PRINTF(x_label_color, x_label, x_msg_color, x_msg, ...) { \
|
||||
US_LOGGING_LOCK; \
|
||||
@@ -111,6 +81,10 @@ extern pthread_mutex_t us_g_log_mutex;
|
||||
US_LOGGING_UNLOCK; \
|
||||
}
|
||||
|
||||
#define US_LOG_SEP_INFO { \
|
||||
US_LOG_PRINTF(US_COLOR_GREEN, "INFO ", US_COLOR_GREEN, "============================================="); \
|
||||
}
|
||||
|
||||
#define US_LOG_ERROR(x_msg, ...) { \
|
||||
US_LOG_PRINTF(US_COLOR_RED, "ERROR", US_COLOR_RED, x_msg, ##__VA_ARGS__); \
|
||||
}
|
||||
@@ -121,6 +95,11 @@ extern pthread_mutex_t us_g_log_mutex;
|
||||
free(m_perror_str); \
|
||||
}
|
||||
|
||||
// We don't include alsa, speex and opus headers here
|
||||
#define US_LOG_PERROR_ALSA(x_err, x_msg, ...) US_LOG_ERROR(x_msg ": %s", ##__VA_ARGS__, snd_strerror(x_err))
|
||||
#define US_LOG_PERROR_RES(x_err, x_msg, ...) US_LOG_ERROR(x_msg ": %s", ##__VA_ARGS__, speex_resampler_strerror(x_err))
|
||||
#define US_LOG_PERROR_OPUS(x_err, x_msg, ...) US_LOG_ERROR(x_msg ": %s", ##__VA_ARGS__, opus_strerror(x_err))
|
||||
|
||||
#define US_LOG_INFO(x_msg, ...) { \
|
||||
US_LOG_PRINTF(US_COLOR_GREEN, "INFO ", "", x_msg, ##__VA_ARGS__); \
|
||||
}
|
||||
|
||||
48
src/libs/logging_base.h
Normal file
48
src/libs/logging_base.h
Normal file
@@ -0,0 +1,48 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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 <pthread.h>
|
||||
|
||||
#include "types.h"
|
||||
#include "tools.h"
|
||||
#include "threading.h"
|
||||
|
||||
|
||||
#define US_LOG_PRINTF_NOLOCK(x_label_color, x_label, x_msg_color, x_msg, ...) { \
|
||||
char m_tname_buf[US_THREAD_NAME_SIZE] = {0}; \
|
||||
us_thread_get_name(m_tname_buf); \
|
||||
if (us_g_log_colored) { \
|
||||
fprintf(stderr, US_COLOR_GRAY "-- " x_label_color x_label US_COLOR_GRAY \
|
||||
" [%.03Lf %9s]" " -- " US_COLOR_RESET x_msg_color x_msg US_COLOR_RESET, \
|
||||
us_get_now_monotonic(), m_tname_buf, ##__VA_ARGS__); \
|
||||
} else { \
|
||||
fprintf(stderr, "-- " x_label " [%.03Lf %9s] -- " x_msg, \
|
||||
us_get_now_monotonic(), m_tname_buf, ##__VA_ARGS__); \
|
||||
} \
|
||||
fputc('\n', stderr); \
|
||||
fflush(stderr); \
|
||||
}
|
||||
215
src/libs/media.c
Normal file
215
src/libs/media.c
Normal file
@@ -0,0 +1,215 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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 "media.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
#include <sys/sysmacros.h>
|
||||
|
||||
#include <linux/videodev2.h>
|
||||
#include <linux/media.h>
|
||||
#include <linux/v4l2-subdev.h>
|
||||
|
||||
#include "types.h"
|
||||
#include "errors.h"
|
||||
#include "tools.h"
|
||||
#include "logging.h"
|
||||
#include "xioctl.h"
|
||||
|
||||
|
||||
#define _LOG_ERROR(x_msg, ...) US_LOG_ERROR("CAP: " x_msg, ##__VA_ARGS__)
|
||||
#define _LOG_PERROR(x_msg, ...) US_LOG_PERROR("CAP: " x_msg, ##__VA_ARGS__)
|
||||
|
||||
|
||||
int us_media_find_link(
|
||||
struct media_v2_topology *topology,
|
||||
u32 link_type,
|
||||
int source_id,
|
||||
int sink_id,
|
||||
u32 *link_flags
|
||||
) {
|
||||
struct media_v2_link *links = (struct media_v2_link *)topology->ptr_links;
|
||||
|
||||
for (uint i = 0; i < topology->num_links; ++i) {
|
||||
if ((links[i].flags & MEDIA_LNK_FL_LINK_TYPE) != link_type) {
|
||||
continue;
|
||||
}
|
||||
if (source_id >= 0 && (u32)source_id == links[i].source_id) {
|
||||
if (link_flags) {
|
||||
*link_flags = links[i].flags;
|
||||
}
|
||||
return links[i].sink_id;
|
||||
}
|
||||
if (sink_id >= 0 && (u32)sink_id == links[i].sink_id) {
|
||||
if (link_flags) {
|
||||
*link_flags = links[i].flags;
|
||||
}
|
||||
return links[i].source_id;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int us_media_find_entity_by_name(
|
||||
const struct media_v2_topology *topology,
|
||||
const char *name
|
||||
) {
|
||||
const struct media_v2_entity *entities = (struct media_v2_entity *)topology->ptr_entities;
|
||||
|
||||
for (uint i = 0; i < topology->num_entities; ++i) {
|
||||
if (!strcmp(entities[i].name, name)) {
|
||||
return entities[i].id;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int us_media_find_entity_by_devnode(
|
||||
struct media_v2_topology *topology,
|
||||
const dev_t devnode
|
||||
) {
|
||||
const struct media_v2_interface *interfaces = (struct media_v2_interface *)topology->ptr_interfaces;
|
||||
|
||||
for (uint i = 0; i < topology->num_interfaces; ++i) {
|
||||
if (
|
||||
interfaces[i].intf_type == MEDIA_INTF_T_V4L_VIDEO
|
||||
&& interfaces[i].devnode.major == major(devnode)
|
||||
&& interfaces[i].devnode.minor == minor(devnode)
|
||||
) {
|
||||
return us_media_find_link(
|
||||
topology, MEDIA_LNK_FL_INTERFACE_LINK,
|
||||
interfaces[i].id, -1, NULL);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
const struct media_v2_pad *us_media_find_pad_by_entity(
|
||||
const struct media_v2_topology *topology,
|
||||
u32 pad_type,
|
||||
u32 entity_id
|
||||
) {
|
||||
const struct media_v2_pad *pads = (const struct media_v2_pad *)topology->ptr_pads;
|
||||
|
||||
for (uint i = 0; i < topology->num_pads; ++i) {
|
||||
if ((pads[i].flags & pad_type) && pads[i].entity_id == entity_id) {
|
||||
return &pads[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const struct media_v2_pad *us_media_find_pad(
|
||||
const struct media_v2_topology *topology,
|
||||
u32 pad_type,
|
||||
u32 pad_id
|
||||
) {
|
||||
const struct media_v2_pad *pads = (const struct media_v2_pad *)topology->ptr_pads;
|
||||
|
||||
for (uint i = 0; i < topology->num_pads; ++i) {
|
||||
if ((pads[i].flags & pad_type) && pads[i].id == pad_id) {
|
||||
return &pads[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int us_media_xioctl_setup_link(
|
||||
int fd,
|
||||
const struct media_v2_pad *source_pad,
|
||||
const struct media_v2_pad *sink_pad,
|
||||
u32 link_flags
|
||||
) {
|
||||
struct media_link_desc link_desc = {
|
||||
.source = {
|
||||
.entity = source_pad->entity_id,
|
||||
.index = source_pad->index,
|
||||
},
|
||||
.sink = {
|
||||
.entity = sink_pad->entity_id,
|
||||
.index = sink_pad->index,
|
||||
},
|
||||
.flags = link_flags,
|
||||
};
|
||||
return us_xioctl(fd, MEDIA_IOC_SETUP_LINK, &link_desc);
|
||||
}
|
||||
|
||||
struct media_v2_topology *us_media_topology_init(int fd) {
|
||||
struct media_v2_topology *topology = NULL;
|
||||
struct media_device_info dev_info = {0};
|
||||
|
||||
if (us_xioctl(fd, MEDIA_IOC_DEVICE_INFO, &dev_info) < 0) {
|
||||
_LOG_PERROR("Can't to query media device info");
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (!MEDIA_V2_PAD_HAS_INDEX(dev_info.media_version)) {
|
||||
_LOG_ERROR("Media topology doesn't have pad indices, too old kernel?");
|
||||
goto error;
|
||||
}
|
||||
|
||||
US_CALLOC(topology, 1);
|
||||
|
||||
if (us_xioctl(fd, MEDIA_IOC_G_TOPOLOGY, topology) < 0) {
|
||||
_LOG_PERROR("Can't query media topology info");
|
||||
goto error;
|
||||
}
|
||||
|
||||
# define INIT_FIELD(x_field, x_type) { \
|
||||
void *m_ptr = calloc(topology->num_##x_field, sizeof(x_type)); \
|
||||
US_A(m_ptr); \
|
||||
topology->ptr_##x_field = (uintptr_t)m_ptr; \
|
||||
}
|
||||
INIT_FIELD(entities, struct media_v2_entity);
|
||||
INIT_FIELD(links, struct media_v2_link);
|
||||
INIT_FIELD(interfaces, struct media_v2_interface);
|
||||
INIT_FIELD(pads, struct media_v2_pad);
|
||||
# undef INIT_FIELD
|
||||
|
||||
if (us_xioctl(fd, MEDIA_IOC_G_TOPOLOGY, topology) < 0) {
|
||||
_LOG_PERROR("Failed to retrieve topology info");
|
||||
goto error;
|
||||
}
|
||||
|
||||
return topology;
|
||||
|
||||
error:
|
||||
US_DELETE(topology, us_media_topology_destroy);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void us_media_topology_destroy(struct media_v2_topology *topology) {
|
||||
# define DELETE_FIELD(x_field) { \
|
||||
if (topology->ptr_##x_field) { \
|
||||
free((void *)topology->ptr_##x_field); \
|
||||
} \
|
||||
}
|
||||
DELETE_FIELD(pads);
|
||||
DELETE_FIELD(interfaces);
|
||||
DELETE_FIELD(links);
|
||||
DELETE_FIELD(entities);
|
||||
# undef DELETE_FIELD
|
||||
free(topology);
|
||||
}
|
||||
70
src/libs/media.h
Normal file
70
src/libs/media.h
Normal file
@@ -0,0 +1,70 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 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 <linux/videodev2.h>
|
||||
#include <linux/media.h>
|
||||
#include <linux/v4l2-subdev.h>
|
||||
|
||||
#include "types.h"
|
||||
|
||||
|
||||
typedef struct {
|
||||
int fd;
|
||||
uint pad;
|
||||
} us_media_pad_s;
|
||||
|
||||
|
||||
int us_media_find_link(
|
||||
struct media_v2_topology *topology,
|
||||
u32 link_type,
|
||||
int source_id,
|
||||
int sink_id,
|
||||
u32 *link_flags);
|
||||
|
||||
int us_media_find_entity_by_name(
|
||||
const struct media_v2_topology *topology,
|
||||
const char *name);
|
||||
|
||||
int us_media_find_entity_by_devnode(
|
||||
struct media_v2_topology *topology,
|
||||
const dev_t devnode);
|
||||
|
||||
const struct media_v2_pad *us_media_find_pad_by_entity(
|
||||
const struct media_v2_topology *topology,
|
||||
u32 pad_type,
|
||||
u32 entity_id);
|
||||
|
||||
const struct media_v2_pad *us_media_find_pad(
|
||||
const struct media_v2_topology *topology,
|
||||
u32 pad_type,
|
||||
u32 pad_id);
|
||||
|
||||
int us_media_xioctl_setup_link(
|
||||
int fd,
|
||||
const struct media_v2_pad *source_pad,
|
||||
const struct media_v2_pad *sink_pad,
|
||||
u32 link_flags);
|
||||
|
||||
struct media_v2_topology *us_media_topology_init(int fd);
|
||||
void us_media_topology_destroy(struct media_v2_topology *topology);
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,11 +22,27 @@
|
||||
|
||||
#include "memsink.h"
|
||||
|
||||
#include <stdatomic.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
|
||||
us_memsink_s *us_memsink_init(
|
||||
#include <sys/file.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/mman.h>
|
||||
|
||||
#include "types.h"
|
||||
#include "errors.h"
|
||||
#include "tools.h"
|
||||
#include "logging.h"
|
||||
#include "frame.h"
|
||||
#include "memsinksh.h"
|
||||
|
||||
|
||||
us_memsink_s *us_memsink_init_opened(
|
||||
const char *name, const char *obj, bool server,
|
||||
mode_t mode, bool rm, unsigned client_ttl, unsigned timeout) {
|
||||
|
||||
mode_t mode, bool rm, uint client_ttl, uint timeout
|
||||
) {
|
||||
us_memsink_s *sink;
|
||||
US_CALLOC(sink, 1);
|
||||
sink->name = name;
|
||||
@@ -40,6 +56,11 @@ us_memsink_s *us_memsink_init(
|
||||
|
||||
US_LOG_INFO("Using %s-sink: %s", name, obj);
|
||||
|
||||
if ((sink->data_size = us_memsinksh_calculate_size(obj)) == 0) {
|
||||
US_LOG_ERROR("%s-sink: Invalid object suffix", name);
|
||||
goto error;
|
||||
}
|
||||
|
||||
const mode_t mask = umask(0);
|
||||
sink->fd = shm_open(sink->obj, (server ? O_RDWR | O_CREAT : O_RDWR), mode);
|
||||
umask(mask);
|
||||
@@ -49,26 +70,25 @@ us_memsink_s *us_memsink_init(
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (sink->server && ftruncate(sink->fd, sizeof(us_memsink_shared_s)) < 0) {
|
||||
if (sink->server && ftruncate(sink->fd, sizeof(us_memsink_shared_s) + sink->data_size) < 0) {
|
||||
US_LOG_PERROR("%s-sink: Can't truncate shared memory", name);
|
||||
goto error;
|
||||
}
|
||||
|
||||
if ((sink->mem = us_memsink_shared_map(sink->fd)) == NULL) {
|
||||
if ((sink->mem = us_memsinksh_map(sink->fd, sink->data_size)) == NULL) {
|
||||
US_LOG_PERROR("%s-sink: Can't mmap shared memory", name);
|
||||
goto error;
|
||||
}
|
||||
|
||||
return sink;
|
||||
|
||||
error:
|
||||
us_memsink_destroy(sink);
|
||||
return NULL;
|
||||
error:
|
||||
us_memsink_destroy(sink);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void us_memsink_destroy(us_memsink_s *sink) {
|
||||
if (sink->mem != NULL) {
|
||||
if (us_memsink_shared_unmap(sink->mem) < 0) {
|
||||
if (us_memsinksh_unmap(sink->mem, sink->data_size) < 0) {
|
||||
US_LOG_PERROR("%s-sink: Can't unmap shared memory", sink->name);
|
||||
}
|
||||
}
|
||||
@@ -86,16 +106,35 @@ void us_memsink_destroy(us_memsink_s *sink) {
|
||||
}
|
||||
|
||||
bool us_memsink_server_check(us_memsink_s *sink, const us_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).
|
||||
// Если frame == NULL, то только проверяем наличие клиентов
|
||||
// или необходимость инициализировать память.
|
||||
|
||||
assert(sink->server);
|
||||
US_A(sink->server);
|
||||
|
||||
if (sink->mem->magic != US_MEMSINK_MAGIC || sink->mem->version != US_MEMSINK_VERSION) {
|
||||
// Если регион памяти не был инициализирован, то нужно что-то туда положить.
|
||||
// Блокировка не нужна, потому что только сервер пишет в эти переменные.
|
||||
return true;
|
||||
}
|
||||
|
||||
const ldf unsafe_ts = sink->mem->last_client_ts;
|
||||
if (unsafe_ts != sink->unsafe_last_client_ts) {
|
||||
// Клиент пишет в синке свою отметку last_client_ts при любом действии.
|
||||
// Мы не берем блокировку здесь, а просто проверяем, является ли это число тем же самым,
|
||||
// что было прочитано нами в предыдущих итерациях. Значению не нужно быть консистентным,
|
||||
// и даже если мы прочитали мусор из-за гонки в памяти между чтением здеси и записью
|
||||
// из клиента, мы все равно можем сделать вывод, есть ли у нас клиенты вообще.
|
||||
// Если число число поменялось то у нас точно есть клиенты и дальнейшие проверки
|
||||
// проводить не требуется. Если же число неизменно, то стоит поставить блокировку
|
||||
// и проверить, нужно ли записать что-нибудь в память для инициализации фрейма.
|
||||
sink->unsafe_last_client_ts = unsafe_ts;
|
||||
atomic_store(&sink->has_clients, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (flock(sink->fd, LOCK_EX | LOCK_NB) < 0) {
|
||||
if (errno == EWOULDBLOCK) {
|
||||
// Есть живой клиент, который прямо сейчас взял блокировку и читает фрейм из синка
|
||||
atomic_store(&sink->has_clients, true);
|
||||
return true;
|
||||
}
|
||||
@@ -103,56 +142,54 @@ bool us_memsink_server_check(us_memsink_s *sink, const us_frame_s *frame) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sink->mem->magic != US_MEMSINK_MAGIC || sink->mem->version != US_MEMSINK_VERSION) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool has_clients = (sink->mem->last_client_ts + sink->client_ttl > us_get_now_monotonic());
|
||||
// Проверяем, есть ли у нас живой клиент по таймауту
|
||||
const bool has_clients = us_memsinksh_has_clients(sink->mem, sink->client_ttl);
|
||||
atomic_store(&sink->has_clients, has_clients);
|
||||
|
||||
if (flock(sink->fd, LOCK_UN) < 0) {
|
||||
US_LOG_PERROR("%s-sink: Can't unlock memory", sink->name);
|
||||
if (us_memsink_server_x_unlock(sink) < 0) {
|
||||
return false;
|
||||
}
|
||||
return (has_clients || !US_FRAME_COMPARE_META_USED_NOTS(sink->mem, frame));;
|
||||
if (has_clients) {
|
||||
return true;
|
||||
}
|
||||
if (frame != NULL && !US_FRAME_COMPARE_GEOMETRY(sink->mem, frame)) {
|
||||
// Если есть изменения в геометрии/формате фрейма, то их тоже нобходимо сразу записать в синк
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int us_memsink_server_put(us_memsink_s *sink, const us_frame_s *frame, bool *const key_requested) {
|
||||
assert(sink->server);
|
||||
int us_memsink_server_put(
|
||||
us_memsink_s *sink,
|
||||
const us_frame_s *frame,
|
||||
us_memsink_wants_s *w_get
|
||||
) {
|
||||
US_A(sink->server);
|
||||
|
||||
const long double now = us_get_now_monotonic();
|
||||
|
||||
if (frame->used > US_MEMSINK_MAX_DATA) {
|
||||
US_LOG_ERROR("%s-sink: Can't put frame: is too big (%zu > %zu)",
|
||||
sink->name, frame->used, US_MEMSINK_MAX_DATA);
|
||||
return 0; // -2
|
||||
}
|
||||
const ldf now = us_get_now_monotonic();
|
||||
|
||||
if (us_flock_timedwait_monotonic(sink->fd, 1) == 0) {
|
||||
const bool has_clients = us_memsinksh_has_clients(sink->mem, sink->client_ttl);
|
||||
atomic_store(&sink->has_clients, has_clients);
|
||||
|
||||
US_LOG_VERBOSE("%s-sink: >>>>> Exposing new frame ...", sink->name);
|
||||
const bool exposed = !us_memsink_server_x_put(sink, frame);
|
||||
|
||||
sink->last_id = us_get_now_id();
|
||||
sink->mem->id = sink->last_id;
|
||||
if (sink->mem->key_requested && frame->key) {
|
||||
sink->mem->key_requested = false;
|
||||
if (w_get != NULL) {
|
||||
if (has_clients) {
|
||||
memcpy(w_get, &sink->mem->wants, sizeof(us_memsink_wants_s));
|
||||
} else {
|
||||
US_MEMSET_ZERO(*w_get);
|
||||
}
|
||||
}
|
||||
*key_requested = sink->mem->key_requested;
|
||||
|
||||
memcpy(sink->mem->data, frame->data, frame->used);
|
||||
sink->mem->used = frame->used;
|
||||
US_FRAME_COPY_META(frame, sink->mem);
|
||||
|
||||
sink->mem->magic = US_MEMSINK_MAGIC;
|
||||
sink->mem->version = US_MEMSINK_VERSION;
|
||||
|
||||
atomic_store(&sink->has_clients, (sink->mem->last_client_ts + sink->client_ttl > us_get_now_monotonic()));
|
||||
|
||||
if (flock(sink->fd, LOCK_UN) < 0) {
|
||||
US_LOG_PERROR("%s-sink: Can't unlock memory", sink->name);
|
||||
if (us_memsink_server_x_unlock(sink) < 0) {
|
||||
return -1;
|
||||
}
|
||||
US_LOG_VERBOSE("%s-sink: Exposed new frame; full exposition time = %.3Lf",
|
||||
sink->name, us_get_now_monotonic() - now);
|
||||
if (exposed) {
|
||||
US_LOG_VERBOSE("%s-sink: Exposed new frame; full exposition time = %.3Lf",
|
||||
sink->name, us_get_now_monotonic() - now);
|
||||
}
|
||||
|
||||
} else if (errno == EWOULDBLOCK) {
|
||||
US_LOG_VERBOSE("%s-sink: ===== Shared memory is busy now; frame skipped", sink->name);
|
||||
@@ -164,42 +201,150 @@ int us_memsink_server_put(us_memsink_s *sink, const us_frame_s *frame, bool *con
|
||||
return 0;
|
||||
}
|
||||
|
||||
int us_memsink_client_get(us_memsink_s *sink, us_frame_s *frame, bool *const key_requested, bool key_required) { // cppcheck-suppress unusedFunction
|
||||
assert(!sink->server); // Client only
|
||||
int us_memsink_client_get(
|
||||
us_memsink_s *sink,
|
||||
us_frame_s *frame,
|
||||
us_memsink_wants_s *w_get,
|
||||
const us_memsink_wants_s *w_put
|
||||
) {
|
||||
US_A(!sink->server); // Client only
|
||||
|
||||
if (us_flock_timedwait_monotonic(sink->fd, sink->timeout) < 0) {
|
||||
if (errno == EWOULDBLOCK) {
|
||||
return -2;
|
||||
return US_ERROR_NO_DATA;
|
||||
}
|
||||
US_LOG_PERROR("%s-sink: Can't lock memory", sink->name);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int retval = -2; // Not updated
|
||||
if (sink->mem->magic == US_MEMSINK_MAGIC) {
|
||||
if (sink->mem->version != US_MEMSINK_VERSION) {
|
||||
US_LOG_ERROR("%s-sink: Protocol version mismatch: sink=%u, required=%u",
|
||||
sink->name, sink->mem->version, US_MEMSINK_VERSION);
|
||||
retval = -1;
|
||||
goto done;
|
||||
}
|
||||
if (sink->mem->id != sink->last_id) { // When updated
|
||||
sink->last_id = sink->mem->id;
|
||||
us_frame_set_data(frame, sink->mem->data, sink->mem->used);
|
||||
US_FRAME_COPY_META(sink->mem, frame);
|
||||
*key_requested = sink->mem->key_requested;
|
||||
retval = 0;
|
||||
}
|
||||
sink->mem->last_client_ts = us_get_now_monotonic();
|
||||
if (key_required) {
|
||||
sink->mem->key_requested = true;
|
||||
int retval = 0;
|
||||
|
||||
if (sink->mem->magic != US_MEMSINK_MAGIC) {
|
||||
retval = US_ERROR_NO_DATA; // Not updated
|
||||
goto done;
|
||||
}
|
||||
if (sink->mem->version != US_MEMSINK_VERSION) {
|
||||
US_LOG_ERROR("%s-sink: Protocol version mismatch: sink=%u, required=%u",
|
||||
sink->name, sink->mem->version, US_MEMSINK_VERSION);
|
||||
retval = -1;
|
||||
goto done;
|
||||
}
|
||||
|
||||
const bool had_client_magic = (sink->mem->client_magic == US_MEMSINK_MAGIC);
|
||||
// Let the sink know that the client is alive
|
||||
sink->mem->last_client_ts = us_get_now_monotonic();
|
||||
sink->mem->client_magic = US_MEMSINK_MAGIC;
|
||||
|
||||
if (sink->mem->used == 0 || sink->mem->id == sink->last_readed_id) {
|
||||
// ^ Zero frame |OR| ^ Not updated
|
||||
retval = US_ERROR_NO_DATA;
|
||||
goto done;
|
||||
}
|
||||
|
||||
sink->last_readed_id = sink->mem->id;
|
||||
us_frame_set_data(frame, us_memsinksh_get_data(sink->mem), sink->mem->used);
|
||||
US_FRAME_COPY_META(sink->mem, frame);
|
||||
|
||||
if (w_get != NULL) {
|
||||
if (had_client_magic) { // Чтобы не прочитать мусор, если не было клиентов
|
||||
memcpy(w_get, &sink->mem->wants, sizeof(us_memsink_wants_s));
|
||||
} else {
|
||||
US_MEMSET_ZERO(*w_get);
|
||||
}
|
||||
}
|
||||
|
||||
done:
|
||||
if (flock(sink->fd, LOCK_UN) < 0) {
|
||||
US_LOG_PERROR("%s-sink: Can't unlock memory", sink->name);
|
||||
if (w_put != NULL) {
|
||||
const bool key = sink->mem->wants.key;
|
||||
memcpy(&sink->mem->wants, w_put, sizeof(us_memsink_wants_s));
|
||||
if (key) {
|
||||
sink->mem->wants.key = key;
|
||||
}
|
||||
} else {
|
||||
US_MEMSET_ZERO(sink->mem->wants);
|
||||
}
|
||||
|
||||
done:
|
||||
if (us_memsink_server_x_unlock(sink) < 0) {
|
||||
retval = -1;
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
us_mss_lock_result_e us_memsink_server_x_lock(
|
||||
us_memsink_s *sink,
|
||||
us_memsink_wants_s *w_get
|
||||
) {
|
||||
US_A(sink->server);
|
||||
|
||||
if (us_flock_timedwait_monotonic(sink->fd, 1) < 0) {
|
||||
if (errno == EWOULDBLOCK) {
|
||||
return US_MSS_BUSY;
|
||||
}
|
||||
US_LOG_PERROR("%s-sink: Can't lock memory", sink->name);
|
||||
return US_MSS_ERROR;
|
||||
}
|
||||
|
||||
if (sink->mem->magic == US_MEMSINK_MAGIC && sink->mem->version == US_MEMSINK_VERSION) {
|
||||
// Если инициализировано - значит клиенту что-то уже могли написать
|
||||
const bool has_clients = us_memsinksh_has_clients(sink->mem, sink->client_ttl);
|
||||
atomic_store(&sink->has_clients, has_clients);
|
||||
if (has_clients) {
|
||||
if (w_get != NULL) {
|
||||
memcpy(w_get, &sink->mem->wants, sizeof(us_memsink_wants_s));
|
||||
}
|
||||
// sink->unsafe_last_client_ts = sink->mem->last_client_ts; // _x_ functions don't use it
|
||||
return US_MSS_SUCCESS;
|
||||
}
|
||||
} else {
|
||||
// А иначе скормим клиентам инициализацию с пустым фреймом,
|
||||
// в следующий раз прочтем от них что-то путное.
|
||||
us_memsink_server_x_put(sink, NULL);
|
||||
}
|
||||
|
||||
if (us_memsink_server_x_unlock(sink) < 0) {
|
||||
return US_MSS_ERROR;
|
||||
}
|
||||
return US_MSS_NO_CLIENT;
|
||||
}
|
||||
|
||||
bool us_memsink_server_x_is_consumed(us_memsink_s *sink) {
|
||||
return (sink->mem->used == 0);
|
||||
}
|
||||
|
||||
void us_memsink_server_x_set_consumed(us_memsink_s *sink) {
|
||||
sink->mem->used = 0;
|
||||
}
|
||||
|
||||
int us_memsink_server_x_put(us_memsink_s *sink, const us_frame_s *frame) {
|
||||
if (frame != NULL) {
|
||||
if (frame->used > sink->data_size) {
|
||||
US_LOG_ERROR("%s-sink: Can't put frame: it's too big (%zu > %zu)",
|
||||
sink->name, frame->used, sink->data_size);
|
||||
return -1;
|
||||
}
|
||||
return retval;
|
||||
|
||||
memcpy(us_memsinksh_get_data(sink->mem), frame->data, frame->used);
|
||||
sink->mem->used = frame->used;
|
||||
US_FRAME_COPY_META(frame, sink->mem);
|
||||
|
||||
if (sink->mem->wants.key && frame->key) {
|
||||
// Можем писать в мусор, но пофигу
|
||||
sink->mem->wants.key = false;
|
||||
}
|
||||
} else {
|
||||
sink->mem->used = 0;
|
||||
}
|
||||
|
||||
sink->mem->id = us_get_now_id();
|
||||
sink->mem->version = US_MEMSINK_VERSION;
|
||||
sink->mem->magic = US_MEMSINK_MAGIC;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int us_memsink_server_x_unlock(us_memsink_s *sink) {
|
||||
if (flock(sink->fd, LOCK_UN) < 0) {
|
||||
US_LOG_PERROR("%s-sink: Can't unlock memory", sink->name);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,47 +22,71 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdatomic.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/mman.h>
|
||||
|
||||
#include "tools.h"
|
||||
#include "logging.h"
|
||||
#include "types.h"
|
||||
#include "frame.h"
|
||||
#include "memsinksh.h"
|
||||
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const char *obj;
|
||||
bool server;
|
||||
bool rm;
|
||||
unsigned client_ttl; // Only for server
|
||||
unsigned timeout;
|
||||
const char *name;
|
||||
const char *obj;
|
||||
uz data_size;
|
||||
bool server;
|
||||
bool rm;
|
||||
uint client_ttl; // Only for server
|
||||
uint timeout;
|
||||
|
||||
int fd;
|
||||
us_memsink_shared_s *mem;
|
||||
uint64_t last_id;
|
||||
atomic_bool has_clients; // Only for server
|
||||
|
||||
u64 last_readed_id; // Only for client
|
||||
|
||||
atomic_bool has_clients; // Only for server results
|
||||
ldf unsafe_last_client_ts; // Only for server
|
||||
} us_memsink_s;
|
||||
|
||||
|
||||
us_memsink_s *us_memsink_init(
|
||||
us_memsink_s *us_memsink_init_opened(
|
||||
const char *name, const char *obj, bool server,
|
||||
mode_t mode, bool rm, unsigned client_ttl, unsigned timeout);
|
||||
mode_t mode, bool rm, uint client_ttl, uint timeout);
|
||||
|
||||
void us_memsink_destroy(us_memsink_s *sink);
|
||||
|
||||
bool us_memsink_server_check(us_memsink_s *sink, const us_frame_s *frame);
|
||||
int us_memsink_server_put(us_memsink_s *sink, const us_frame_s *frame, bool *const key_requested);
|
||||
|
||||
int us_memsink_client_get(us_memsink_s *sink, us_frame_s *frame, bool *const key_requested, bool key_required);
|
||||
int us_memsink_server_put(
|
||||
us_memsink_s *sink,
|
||||
const us_frame_s *frame,
|
||||
us_memsink_wants_s *w_get);
|
||||
|
||||
int us_memsink_client_get(
|
||||
us_memsink_s *sink,
|
||||
us_frame_s *frame,
|
||||
us_memsink_wants_s *w_get,
|
||||
const us_memsink_wants_s *w_put);
|
||||
|
||||
|
||||
// Low-level
|
||||
|
||||
typedef enum {
|
||||
US_MSS_SUCCESS = 0,
|
||||
US_MSS_ERROR = -1,
|
||||
US_MSS_BUSY = -2,
|
||||
US_MSS_NO_CLIENT = -3,
|
||||
} us_mss_lock_result_e;
|
||||
|
||||
|
||||
us_mss_lock_result_e us_memsink_server_x_lock(
|
||||
us_memsink_s *sink,
|
||||
us_memsink_wants_s *w_get);
|
||||
|
||||
bool us_memsink_server_x_is_consumed(us_memsink_s *sink);
|
||||
void us_memsink_server_x_set_consumed(us_memsink_s *sink);
|
||||
|
||||
int us_memsink_server_x_put(us_memsink_s *sink, const us_frame_s *frame);
|
||||
|
||||
int us_memsink_server_x_unlock(us_memsink_s *sink);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -20,49 +20,60 @@
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
#include "unix.h"
|
||||
#include "memsinksh.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include <sys/mman.h>
|
||||
|
||||
#include "types.h"
|
||||
#include "tools.h"
|
||||
|
||||
|
||||
evutil_socket_t us_evhttp_bind_unix(struct evhttp *http, const char *path, bool rm, mode_t mode) {
|
||||
struct sockaddr_un addr = {0};
|
||||
|
||||
# define MAX_SUN_PATH (sizeof(addr.sun_path) - 1)
|
||||
|
||||
if (strlen(path) > MAX_SUN_PATH) {
|
||||
US_LOG_ERROR("UNIX socket path is too long; max=%zu", MAX_SUN_PATH);
|
||||
return -1;
|
||||
us_memsink_shared_s *us_memsinksh_map(int fd, uz data_size) {
|
||||
us_memsink_shared_s *mem = mmap(
|
||||
NULL,
|
||||
sizeof(us_memsink_shared_s) + data_size,
|
||||
PROT_READ | PROT_WRITE, MAP_SHARED,
|
||||
fd, 0);
|
||||
if (mem == MAP_FAILED) {
|
||||
return NULL;
|
||||
}
|
||||
US_A(mem != NULL);
|
||||
return mem;
|
||||
}
|
||||
|
||||
strncpy(addr.sun_path, path, MAX_SUN_PATH);
|
||||
addr.sun_family = AF_UNIX;
|
||||
int us_memsinksh_unmap(us_memsink_shared_s *mem, uz data_size) {
|
||||
US_A(mem != NULL);
|
||||
return munmap(mem, sizeof(us_memsink_shared_s) + data_size);
|
||||
}
|
||||
|
||||
# undef MAX_SUN_PATH
|
||||
|
||||
const evutil_socket_t fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
assert(fd >= 0);
|
||||
assert(!evutil_make_socket_nonblocking(fd));
|
||||
|
||||
if (rm && unlink(path) < 0) {
|
||||
if (errno != ENOENT) {
|
||||
US_LOG_PERROR("Can't remove old UNIX socket '%s'", path);
|
||||
return -1;
|
||||
uz us_memsinksh_calculate_size(const char *obj) {
|
||||
const char *ptr = strrchr(obj, ':');
|
||||
if (ptr == NULL) {
|
||||
ptr = strrchr(obj, '.');
|
||||
}
|
||||
if (ptr != NULL) {
|
||||
ptr += 1;
|
||||
if (!strcasecmp(ptr, "jpeg")) {
|
||||
return 4 * 1024 * 1024;
|
||||
} else if (!strcasecmp(ptr, "h264")) {
|
||||
return 2 * 1024 * 1024;
|
||||
} else if (!strcasecmp(ptr, "raw")) {
|
||||
return 3840 * 2160 * 3; // RGB
|
||||
}
|
||||
}
|
||||
if (bind(fd, (struct sockaddr *)&addr, sizeof(struct sockaddr_un)) < 0) {
|
||||
US_LOG_PERROR("Can't bind HTTP to UNIX socket '%s'", path);
|
||||
return -1;
|
||||
}
|
||||
if (mode && chmod(path, mode) < 0) {
|
||||
US_LOG_PERROR("Can't set permissions %o to UNIX socket '%s'", mode, path);
|
||||
return -1;
|
||||
}
|
||||
if (listen(fd, 128) < 0) {
|
||||
US_LOG_PERROR("Can't listen UNIX socket '%s'", path);
|
||||
return -1;
|
||||
}
|
||||
if (evhttp_accept_socket(http, fd) < 0) {
|
||||
US_LOG_PERROR("Can't evhttp_accept_socket() UNIX socket '%s'", path);
|
||||
return -1;
|
||||
}
|
||||
return fd;
|
||||
return 0;
|
||||
}
|
||||
|
||||
u8 *us_memsinksh_get_data(us_memsink_shared_s *mem) {
|
||||
return (u8*)(mem) + sizeof(us_memsink_shared_s);
|
||||
}
|
||||
|
||||
bool us_memsinksh_has_clients(const us_memsink_shared_s *mem, u32 ttl) {
|
||||
return (
|
||||
mem->client_magic == US_MEMSINK_MAGIC
|
||||
&& mem->last_client_ts + ttl > us_get_now_monotonic()
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,65 +22,39 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/mman.h>
|
||||
#include "types.h"
|
||||
#include "frame.h"
|
||||
|
||||
|
||||
#define US_MEMSINK_MAGIC ((uint64_t)0xCAFEBABECAFEBABE)
|
||||
#define US_MEMSINK_VERSION ((uint32_t)4)
|
||||
|
||||
#ifndef US_CFG_MEMSINK_MAX_DATA
|
||||
# define US_CFG_MEMSINK_MAX_DATA 33554432
|
||||
#endif
|
||||
#define US_MEMSINK_MAX_DATA ((size_t)(US_CFG_MEMSINK_MAX_DATA))
|
||||
#define US_MEMSINK_MAGIC ((u64)0xCAFEBABECAFEBABE)
|
||||
#define US_MEMSINK_VERSION ((u32)11)
|
||||
|
||||
|
||||
typedef struct {
|
||||
uint64_t magic;
|
||||
uint32_t version;
|
||||
uint width;
|
||||
uint height;
|
||||
uint format;
|
||||
uint fps;
|
||||
bool key;
|
||||
} us_memsink_wants_s;
|
||||
|
||||
uint64_t id;
|
||||
typedef struct {
|
||||
u64 magic;
|
||||
u32 version;
|
||||
u64 id;
|
||||
uz used;
|
||||
|
||||
size_t used;
|
||||
unsigned width;
|
||||
unsigned height;
|
||||
unsigned format;
|
||||
unsigned stride;
|
||||
bool online;
|
||||
bool key;
|
||||
unsigned gop;
|
||||
u64 client_magic;
|
||||
ldf last_client_ts;
|
||||
us_memsink_wants_s wants;
|
||||
|
||||
long double grab_ts;
|
||||
long double encode_begin_ts;
|
||||
long double encode_end_ts;
|
||||
|
||||
long double last_client_ts;
|
||||
bool key_requested;
|
||||
|
||||
uint8_t data[US_MEMSINK_MAX_DATA];
|
||||
US_FRAME_META_DECLARE;
|
||||
} us_memsink_shared_s;
|
||||
|
||||
|
||||
INLINE us_memsink_shared_s *us_memsink_shared_map(int fd) {
|
||||
us_memsink_shared_s *mem = mmap(
|
||||
NULL,
|
||||
sizeof(us_memsink_shared_s),
|
||||
PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED,
|
||||
fd,
|
||||
0
|
||||
);
|
||||
if (mem == MAP_FAILED) {
|
||||
return NULL;
|
||||
}
|
||||
assert(mem != NULL);
|
||||
return mem;
|
||||
}
|
||||
us_memsink_shared_s *us_memsinksh_map(int fd, uz data_size);
|
||||
int us_memsinksh_unmap(us_memsink_shared_s *mem, uz data_size);
|
||||
|
||||
INLINE int us_memsink_shared_unmap(us_memsink_shared_s *mem) {
|
||||
assert(mem != NULL);
|
||||
return munmap(mem, sizeof(us_memsink_shared_s));
|
||||
}
|
||||
uz us_memsinksh_calculate_size(const char *obj);
|
||||
u8 *us_memsinksh_get_data(us_memsink_shared_s *mem);
|
||||
bool us_memsinksh_has_clients(const us_memsink_shared_s *mem, u32 ttl);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,17 +22,24 @@
|
||||
|
||||
#include "options.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <getopt.h>
|
||||
|
||||
void us_build_short_options(const struct option opts[], char *short_opts, size_t size) {
|
||||
#include "types.h"
|
||||
#include "tools.h"
|
||||
|
||||
|
||||
void us_build_short_options(const struct option opts[], char *short_opts, uz 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;
|
||||
for (uint short_i = 0, opt_i = 0; opts[opt_i].name != NULL; ++opt_i) {
|
||||
US_A(short_i < size - 3);
|
||||
if (isalpha(opts[opt_i].val)) {
|
||||
short_opts[short_i] = opts[opt_i].val;
|
||||
++short_i;
|
||||
if (opts[opt_i].has_arg == required_argument) {
|
||||
short_opts[short_i] = ':';
|
||||
++short_i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# Copyright (C) 2018-2024 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 #
|
||||
@@ -22,12 +22,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <getopt.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
#include "types.h"
|
||||
|
||||
|
||||
void us_build_short_options(const struct option opts[], char *short_opts, size_t size);
|
||||
void us_build_short_options(const struct option opts[], char *short_opts, uz size);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user