mirror of
https://github.com/pikvm/ustreamer.git
synced 2026-02-19 08:16:31 +00:00
Compare commits
443 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
f8ed7d7b3b | ||
|
|
622f5cf1eb | ||
|
|
81756811f3 | ||
|
|
bd403593a0 | ||
|
|
fc3e0232e1 | ||
|
|
89fd83bfc1 | ||
|
|
83a77ea898 | ||
|
|
33fdf9bf43 | ||
|
|
6bd4ef59c0 | ||
|
|
79987da1bf | ||
|
|
05e5db09e4 | ||
|
|
55e432a529 | ||
|
|
4732c85ec4 | ||
|
|
0ce7f28754 | ||
|
|
a2641dfcb6 | ||
|
|
ec33425c05 | ||
|
|
a4b4dd3932 | ||
|
|
e952f787a0 | ||
|
|
b3e4ea9c0f | ||
|
|
22a816b9b5 | ||
|
|
c96559e4ac | ||
|
|
a52df47b29 | ||
|
|
68e7e97e74 | ||
|
|
35ed415f4c | ||
|
|
121edf5a10 | ||
|
|
aa90ed1fbb | ||
|
|
a102a4a3db | ||
|
|
516c0be6ea | ||
|
|
0745f0a75a | ||
|
|
90e51c0619 | ||
|
|
cb9c1658af | ||
|
|
548c261d92 | ||
|
|
d4560fcba9 | ||
|
|
370434601c | ||
|
|
09359cb957 | ||
|
|
71b93a2a38 | ||
|
|
aeb5930483 | ||
|
|
b17b87018b | ||
|
|
602ca16178 | ||
|
|
28c8599167 | ||
|
|
aa668cec9d | ||
|
|
f6ec0ade38 | ||
|
|
a10df2f01f | ||
|
|
dde2190ac9 | ||
|
|
c24c1ec5ff | ||
|
|
cfd0cdad59 | ||
|
|
31219358c5 | ||
|
|
4adfadfaea | ||
|
|
6174edcf0d | ||
|
|
05d9322404 | ||
|
|
bf78d8f562 | ||
|
|
77a5dbfeae | ||
|
|
3eebeaeedd | ||
|
|
aba8396d60 | ||
|
|
5b33246b6b | ||
|
|
7f3f480d92 | ||
|
|
86fef47018 | ||
|
|
f09bb1ade9 | ||
|
|
fa030147e8 | ||
|
|
f88333b6bf | ||
|
|
9b4f3229f2 | ||
|
|
900d7e1112 | ||
|
|
c0588c6736 | ||
|
|
5bf8c97a1c | ||
|
|
7335a5d2df | ||
|
|
839804b476 | ||
|
|
5bb223506a | ||
|
|
414f536ace | ||
|
|
c1363d55e0 | ||
|
|
c871229740 | ||
|
|
01de2caa97 | ||
|
|
d154a3d1c1 | ||
|
|
b111487757 | ||
|
|
47d1c09377 | ||
|
|
3d2f254f40 | ||
|
|
13e31d0cd5 | ||
|
|
4e7676f307 | ||
|
|
95fcc3c58e | ||
|
|
81500af1b3 | ||
|
|
86a2141361 | ||
|
|
22e5c8627b | ||
|
|
c8600e62c2 | ||
|
|
335f19f0e3 | ||
|
|
a24e0eeb86 | ||
|
|
3fcb8d3ee5 | ||
|
|
ca30656bf8 | ||
|
|
4f0abf7eec | ||
|
|
8b233a4c71 | ||
|
|
8a81158276 | ||
|
|
f83ff439c4 | ||
|
|
983796e952 | ||
|
|
40be0c20e2 | ||
|
|
f1e9d4568c | ||
|
|
f4f57cce38 | ||
|
|
c87ad5703c | ||
|
|
95df13b7cb | ||
|
|
bfa1516491 | ||
|
|
36c9ff22b3 | ||
|
|
17f54a7977 | ||
|
|
574986d0fd | ||
|
|
d5ce2e835f | ||
|
|
6201554ba1 | ||
|
|
75dee4e91d | ||
|
|
e1b4e0db66 | ||
|
|
0d37b09bb4 | ||
|
|
18767b68ff | ||
|
|
7975615c6c | ||
|
|
90f09b197e | ||
|
|
687e97d523 | ||
|
|
c1675001fa | ||
|
|
69cc45a2a0 | ||
|
|
ece96b5834 | ||
|
|
383ed7530b | ||
|
|
7f620c758f | ||
|
|
fb50eea526 | ||
|
|
63f757a9da | ||
|
|
4ec02d46b9 | ||
|
|
527afb66df | ||
|
|
5502758a7e | ||
|
|
faa1776407 | ||
|
|
3d7fb8c8dd | ||
|
|
b5f814d71e | ||
|
|
6eafd4156a | ||
|
|
df1e4eaa06 | ||
|
|
d2bef81b03 | ||
|
|
7b3dffd072 | ||
|
|
1a6e9998fb | ||
|
|
9ab9561803 | ||
|
|
11f0b80228 | ||
|
|
85e2dbd69e | ||
|
|
b693c24411 | ||
|
|
54af47fc43 | ||
|
|
1c1e3b0875 | ||
|
|
2c9334d53f | ||
|
|
5c747a5b5d | ||
|
|
cbee3adb2e | ||
|
|
e3293d6887 | ||
|
|
9d1a42631e | ||
|
|
13f522e81d | ||
|
|
28f13f7514 | ||
|
|
2f86f818cc | ||
|
|
1ffcd83993 | ||
|
|
9b46c2e597 | ||
|
|
ad1b63890a | ||
|
|
ad79bd0957 | ||
|
|
021823bcba | ||
|
|
7b0e171e74 | ||
|
|
b24f106ce7 | ||
|
|
20f056668f | ||
|
|
f9439c785f | ||
|
|
5e364fb88b | ||
|
|
69dc9b8b49 | ||
|
|
42237d9728 | ||
|
|
17bd25d497 | ||
|
|
a2ac1f8067 | ||
|
|
fdb1b2d562 | ||
|
|
fd2bf5ea25 | ||
|
|
c874929e9d | ||
|
|
db5b9d3cd7 | ||
|
|
12ab66be43 | ||
|
|
27d25a59d8 | ||
|
|
71991254a5 | ||
|
|
50e8469a59 | ||
|
|
3f45debca0 | ||
|
|
627b614ab5 | ||
|
|
f11d390b22 | ||
|
|
f1e50b6f9b | ||
|
|
fdf3340a7d | ||
|
|
02513be220 | ||
|
|
d29ce42f08 | ||
|
|
aa6fc7fe04 | ||
|
|
c91341a375 | ||
|
|
3de7e26a36 | ||
|
|
63cc66e8a7 | ||
|
|
92a090dec3 | ||
|
|
8b0ef8a271 | ||
|
|
a360f1901e | ||
|
|
ed2d5f3af4 | ||
|
|
b935dd1fe8 | ||
|
|
6e1f60a36d | ||
|
|
210dfcfa4f | ||
|
|
ec10a9e3fe | ||
|
|
217d146378 | ||
|
|
3e2a43e2af | ||
|
|
2e0a19c1cb | ||
|
|
054748234e | ||
|
|
53873e9ddb | ||
|
|
c21d0aef7e | ||
|
|
e505a56910 | ||
|
|
f4278f32c4 | ||
|
|
e9a6db02f6 | ||
|
|
63fe32ddd9 | ||
|
|
710652073a | ||
|
|
dded49cd83 | ||
|
|
0f753dc654 | ||
|
|
c505a423af | ||
|
|
1cff2545b1 | ||
|
|
3d994d6e67 | ||
|
|
7175f8d569 | ||
|
|
d4a9862a18 | ||
|
|
3d4e9fbb1a | ||
|
|
4a59b038ec | ||
|
|
6795744aea | ||
|
|
798fe04905 | ||
|
|
1460de95c1 | ||
|
|
358950d0c2 | ||
|
|
caef82d96f | ||
|
|
dff7dd7087 | ||
|
|
79bb881a34 | ||
|
|
f0763e3865 | ||
|
|
a8dfa96db0 | ||
|
|
7ceb8a3da5 | ||
|
|
9344059e0e | ||
|
|
ee2b1afe5b | ||
|
|
31f47f2eac | ||
|
|
69a9bafcd5 | ||
|
|
dbecdf5d9b | ||
|
|
967574a78a | ||
|
|
bb4f6f3993 | ||
|
|
fdc955a713 | ||
|
|
322c67f7a5 | ||
|
|
7aa7f43c7e | ||
|
|
8ef5505a48 | ||
|
|
481f2c9479 | ||
|
|
924ebf7c77 | ||
|
|
d67af7a9dc | ||
|
|
e6be119ab5 | ||
|
|
ead06e741a | ||
|
|
fbabba29ed | ||
|
|
ff9b0f5396 | ||
|
|
52a31f619b | ||
|
|
743e8ac828 | ||
|
|
cecec39281 | ||
|
|
280254e231 | ||
|
|
97a1aa04cb | ||
|
|
e14644572b | ||
|
|
55e0a096e7 | ||
|
|
e68e6b6fae | ||
|
|
aa03e1610f | ||
|
|
2b969dd20d | ||
|
|
c51984c9d9 | ||
|
|
459060532a | ||
|
|
016ee1c554 | ||
|
|
26e0c9d54c | ||
|
|
e6584da7c8 | ||
|
|
630ad66cc8 | ||
|
|
222b9a0309 | ||
|
|
235a1765d2 | ||
|
|
bff8a5d854 | ||
|
|
fd65ed006a | ||
|
|
52478df3cf | ||
|
|
7732841cdf | ||
|
|
10aa33d899 | ||
|
|
bd6fa7b0dc | ||
|
|
536f45eff5 | ||
|
|
bb1dcc005c | ||
|
|
ea313d3517 | ||
|
|
78a12f7ed2 | ||
|
|
281f4eb3a0 | ||
|
|
dabaab4987 | ||
|
|
61ab2a81a5 | ||
|
|
d9b511c69f | ||
|
|
4d9e72e313 | ||
|
|
616a3eb6a6 | ||
|
|
3a8f035014 | ||
|
|
be2e0f11da | ||
|
|
fec76dc9eb | ||
|
|
48826208fd | ||
|
|
dd4fda6f5d | ||
|
|
1dafb54621 | ||
|
|
19f9567098 | ||
|
|
b3d1f06e5d | ||
|
|
f17069153d | ||
|
|
8a7d7b9c54 | ||
|
|
df7649d56f | ||
|
|
58cc227cba | ||
|
|
76be4a1d10 | ||
|
|
496c9300fc | ||
|
|
5b7780cf7c |
@@ -1,18 +1,18 @@
|
||||
[bumpversion]
|
||||
commit = True
|
||||
tag = True
|
||||
current_version = 4.10
|
||||
current_version = 6.9
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)
|
||||
serialize =
|
||||
{major}.{minor}
|
||||
|
||||
[bumpversion:file:src/libs/config.h]
|
||||
[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/config.h]
|
||||
[bumpversion:file:./src/libs/const.h]
|
||||
parse = <major>\d+\.(?P<minor>\d+)
|
||||
serialize = {minor}
|
||||
search = VERSION_MINOR {current_version}
|
||||
|
||||
@@ -7,3 +7,4 @@
|
||||
!python/**
|
||||
!janus/**
|
||||
!man/**
|
||||
!pkg/docker/entry.sh
|
||||
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,4 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
patreon: pikvm
|
||||
custom: https://www.paypal.me/mdevaev
|
||||
custom: https://paypal.me/pikvm
|
||||
|
||||
81
.github/workflows/docker-alpine-image.yaml
vendored
Normal file
81
.github/workflows/docker-alpine-image.yaml
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
name: Build Alpine
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, build_*]
|
||||
tags: [v*]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
buildx:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Login to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Login to DockerHub Container Registry
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKERHUB_REPO }}/ustreamer,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
ghcr.io/${{ github.repository }},enable=${{ github.event_name != 'pull_request' }}
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=sha,format=long
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
driver-opts: image=moby/buildkit:master
|
||||
-
|
||||
name: Build and export to Docker
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
load: true
|
||||
tags: ustreamer
|
||||
file: pkg/docker/Dockerfile.alpine
|
||||
-
|
||||
name: Test
|
||||
run: |
|
||||
echo version: $(docker run --rm -t ustreamer --version)
|
||||
echo -e "features:\n$(docker run --rm -t ustreamer --features)"
|
||||
-
|
||||
name: Build multi arch
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/arm64,linux/amd64,linux/arm/v6,linux/arm/v7
|
||||
file: pkg/docker/Dockerfile.alpine
|
||||
-
|
||||
name: Push
|
||||
if: steps.meta.outputs.tags != ''
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/arm64,linux/amd64,linux/arm/v7
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
file: pkg/docker/Dockerfile.alpine
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -3,10 +3,12 @@
|
||||
/pkg/arch/pkg/
|
||||
/pkg/arch/src/
|
||||
/src/build/
|
||||
/python/build/
|
||||
/python/dist/
|
||||
/python/ustreamer.egg-info/
|
||||
/python/root/
|
||||
/janus/build/
|
||||
/ustreamer
|
||||
/ustreamer-dump
|
||||
/ustreamer-*
|
||||
/config.mk
|
||||
vgcore.*
|
||||
*.sock
|
||||
|
||||
27
Makefile
27
Makefile
@@ -9,8 +9,7 @@ PY ?= python3
|
||||
CFLAGS ?= -O3
|
||||
LDFLAGS ?=
|
||||
|
||||
RPI_VC_HEADERS ?= /opt/vc/include
|
||||
RPI_VC_LIBS ?= /opt/vc/lib
|
||||
R_DESTDIR = $(if $(DESTDIR),$(shell realpath "$(DESTDIR)"),)
|
||||
|
||||
export
|
||||
|
||||
@@ -22,6 +21,9 @@ define optbool
|
||||
$(filter $(shell echo $(1) | tr A-Z a-z), yes on 1)
|
||||
endef
|
||||
|
||||
ifeq ($(V),)
|
||||
ECHO = @
|
||||
endif
|
||||
|
||||
# =====
|
||||
all:
|
||||
@@ -36,18 +38,19 @@ 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/root/usr/lib/python*/site-packages/*.so .
|
||||
|
||||
|
||||
janus:
|
||||
$(MAKE) -C janus
|
||||
@ ln -sf janus/*.so .
|
||||
$(ECHO) ln -sf janus/*.so .
|
||||
|
||||
|
||||
install: all
|
||||
@@ -58,10 +61,10 @@ endif
|
||||
ifneq ($(call optbool,$(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,8 +73,8 @@ install-strip: install
|
||||
|
||||
|
||||
regen:
|
||||
tools/$(MAKE)-jpeg-h.py src/ustreamer/data/blank.jpeg src/ustreamer/data/blank_jpeg.c BLANK
|
||||
tools/$(MAKE)-html-h.py src/ustreamer/data/index.html src/ustreamer/data/index_html.c INDEX
|
||||
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:
|
||||
@@ -118,7 +121,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
|
||||
|
||||
124
README.md
124
README.md
@@ -2,21 +2,20 @@
|
||||
[](https://github.com/pikvm/ustreamer/actions?query=workflow%3ACI)
|
||||
[](https://discord.gg/bpmXfz5)
|
||||
|
||||
[[Русская версия]](README.ru.md)
|
||||
|
||||
µStreamer is a lightweight and very quick server to stream [MJPG](https://en.wikipedia.org/wiki/Motion_JPEG) video from any V4L2 device to the net. All new browsers have native support of this video format, as well as most video players such as mplayer, VLC etc.
|
||||
µStreamer is a part of the [Pi-KVM](https://github.com/pikvm/pikvm) project designed to stream [VGA](https://www.amazon.com/dp/B0126O0RDC) and [HDMI](https://auvidea.com/b101-hdmi-to-csi-2-bridge-15-pin-fpc/) screencast hardware data with the highest resolution and FPS possible.
|
||||
µStreamer is a lightweight and very quick server to stream [MJPEG](https://en.wikipedia.org/wiki/Motion_JPEG) video from any V4L2 device to the net. All new browsers have native support of this video format, as well as most video players such as mplayer, VLC etc.
|
||||
µStreamer is a part of the [PiKVM](https://github.com/pikvm/pikvm) project designed to stream [VGA](https://www.amazon.com/dp/B0126O0RDC) and [HDMI](https://auvidea.com/b101-hdmi-to-csi-2-bridge-15-pin-fpc/) screencast hardware data with the highest resolution and FPS possible.
|
||||
|
||||
µStreamer is very similar to [mjpg-streamer](https://github.com/jacksonliam/mjpg-streamer) with ```input_uvc.so``` and ```output_http.so``` plugins, however, there are some major differences. The key ones are:
|
||||
|
||||
| **Feature** | **µStreamer** | **mjpg-streamer** |
|
||||
|----------|---------------|-------------------|
|
||||
| Multithreaded JPEG encoding | ✔ | ✘ |
|
||||
| [OpenMAX IL](https://www.khronos.org/openmaxil) hardware acceleration<br>on Raspberry Pi | ✔ | ✘ |
|
||||
| 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> |
|
||||
| [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 | ✔ | ✘ |
|
||||
| Systemd socket activation | ✔ | ✘ |
|
||||
| Debug logs without recompiling,<br>performance statistics log,<br>access to HTTP streaming parameters | ✔ | ✘ |
|
||||
| Option to serve files<br>with a built-in HTTP server | ✔ | ☹ Regular files only |
|
||||
| Signaling about the stream state<br>on GPIO using [libgpiod](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/about) | ✔ | ✘ |
|
||||
@@ -33,14 +32,30 @@ Footnotes:
|
||||
If you're going to live-stream from your backyard webcam and need to control it, use mjpg-streamer. If you need a high-quality image with high FPS - µStreamer for the win.
|
||||
|
||||
-----
|
||||
# Building
|
||||
You'll need ```make```, ```gcc```, ```libevent``` with ```pthreads``` support, ```libjpeg8```/```libjpeg-turbo``` and ```libbsd``` (only for Linux).
|
||||
# Installation
|
||||
|
||||
## Building
|
||||
You need to download the µStreamer onto your system and build it from the sources.
|
||||
|
||||
* 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.
|
||||
* Debian: https://packages.debian.org/sid/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).
|
||||
|
||||
* Arch: `sudo pacman -S libevent libjpeg-turbo libutil-linux libbsd`.
|
||||
* Raspbian: `sudo apt install libevent-dev libjpeg8-dev libbsd-dev`. Add `libraspberrypi-dev` for `WITH_OMX=1` and `libgpiod-dev` for `WITH_GPIO=1`.
|
||||
* 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`.
|
||||
* Raspberry OS Bookworm: same as previous but replace `libjpeg62-turbo` to `libjpeg62-turbo-dev`.
|
||||
* Debian/Ubuntu: `sudo apt install build-essential libevent-dev libjpeg-dev libbsd-dev`.
|
||||
* Alpine: `sudo apk add libevent-dev libbsd-dev libjpeg-turbo-dev musl-dev`. Build with `WITH_PTHREAD_NP=0`.
|
||||
|
||||
On Raspberry Pi you can build the program with OpenMAX IL. To do this pass option ```WITH_OMX=1``` to ```make```. To enable GPIO support install [libgpiod](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/about) and pass option ```WITH_GPIO=1```. If the compiler reports about a missing function ```pthread_get_name_np()``` (or similar), add option ```WITH_PTHREAD_NP=0``` (it's enabled by default). For the similar error with ```setproctitle()``` add option ```WITH_SETPROCTITLE=0```.
|
||||
To enable GPIO support install [libgpiod](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/about) and pass option ```WITH_GPIO=1```. If the compiler reports about a missing function ```pthread_get_name_np()``` (or similar), add option ```WITH_PTHREAD_NP=0``` (it's enabled by default). For the similar error with ```setproctitle()``` add option ```WITH_SETPROCTITLE=0```.
|
||||
|
||||
### Make
|
||||
The most convenient process is to clone the µStreamer Git repository onto your system. If you don't have Git installed and don't want to install it either, you can download and unzip the sources from GitHub using `wget https://github.com/pikvm/ustreamer/archive/refs/heads/master.zip`.
|
||||
|
||||
```
|
||||
$ git clone --depth=1 https://github.com/pikvm/ustreamer
|
||||
@@ -49,11 +64,20 @@ $ make
|
||||
$ ./ustreamer --help
|
||||
```
|
||||
|
||||
AUR has a package for Arch Linux: https://aur.archlinux.org/packages/ustreamer. It should compile automatically with OpenMAX IL on Raspberry Pi, if the corresponding headers are present in ```/opt/vc/include```.
|
||||
FreeBSD port: https://www.freshports.org/multimedia/ustreamer.
|
||||
## Update
|
||||
Assuming you have a µStreamer clone as discussed above you can update µStreamer as follows.
|
||||
|
||||
```
|
||||
$ cd ustreamer
|
||||
$ git pull
|
||||
$ make clean
|
||||
$ make
|
||||
```
|
||||
|
||||
-----
|
||||
# Usage
|
||||
**For M2M hardware encoding on Raspberry Pi, you need at least 5.15.32 kernel. OpenMAX and MMAL support on older kernels is deprecated and removed.**
|
||||
|
||||
Without arguments, ```ustreamer``` will try to open ```/dev/video0``` with 640x480 resolution and start streaming on ```http://127.0.0.1:8080```. You can override this behavior using parameters ```--device```, ```--host``` and ```--port```. For example, to stream to the world, run:
|
||||
```
|
||||
# ./ustreamer --device=/dev/video1 --host=0.0.0.0 --port=80
|
||||
@@ -61,13 +85,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:
|
||||
```bash
|
||||
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=omx \ # Hardware encoding with OpenMAX
|
||||
--workers=3 \ # Maximum workers for OpenMAX
|
||||
--persistent \ # Don't re-initialize device on timeout (for example when HDMI cable was disconnected)
|
||||
--encoder=m2m-image \ # Hardware encoding on V4L2 M2M driver
|
||||
--workers=3 \ # Workers number
|
||||
--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
|
||||
```
|
||||
@@ -76,10 +100,67 @@ $ ./ustreamer \
|
||||
|
||||
You can always view the full list of options with ```ustreamer --help```.
|
||||
|
||||
-----
|
||||
# Docker (Raspberry Pi 4 HDMI)
|
||||
|
||||
## Preparations
|
||||
Add following lines to /boot/firmware/usercfg.txt:
|
||||
|
||||
```
|
||||
gpu_mem=128
|
||||
dtoverlay=tc358743
|
||||
```
|
||||
|
||||
Check size of CMA:
|
||||
|
||||
```
|
||||
$ dmesg | grep cma-reserved
|
||||
[ 0.000000] Memory: 7700524K/8244224K available (11772K kernel code, 1278K rwdata, 4320K rodata, 4096K init, 1077K bss, 281556K reserved, 262144K cma-reserved)
|
||||
```
|
||||
|
||||
If it is smaller than 128M add following to /boot/firmware/cmdline.txt:
|
||||
|
||||
```
|
||||
cma=128M
|
||||
```
|
||||
|
||||
Save changes and reboot.
|
||||
|
||||
## Launch
|
||||
Start container:
|
||||
|
||||
```
|
||||
$ docker run --device /dev/video0:/dev/video0 -e EDID=1 -p 8080:8080 pikvm/ustreamer:latest
|
||||
```
|
||||
|
||||
Then access the web interface at port 8080 (e.g. http://raspberrypi.local:8080).
|
||||
|
||||
## Custom config
|
||||
```
|
||||
$ docker run --rm pikvm/ustreamer:latest \
|
||||
--format=uyvy \
|
||||
--workers=3 \
|
||||
--persistent \
|
||||
--dv-timings \
|
||||
--drop-same-frames=30
|
||||
```
|
||||
|
||||
## EDID
|
||||
Add `-e EDID=1` to set HDMI EDID before starting ustreamer. Use together with `-e EDID_HEX=xx` to specify custom EDID data.
|
||||
|
||||
-----
|
||||
# Raspberry Pi Camera Example
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
For v2 camera you can use the same trick with `libcamerify` but enable legacy camera mode in `raspi-config`.
|
||||
|
||||
Example usage for the Raspberry Pi v1 camera:
|
||||
```bash
|
||||
```
|
||||
$ sudo modprobe bcm2835-v4l2
|
||||
$ ./ustreamer --host :: -m jpeg --device-timeout=5 --buffers=3 -r 2592x1944
|
||||
```
|
||||
@@ -88,15 +169,18 @@ $ ./ustreamer --host :: -m jpeg --device-timeout=5 --buffers=3 -r 2592x1944
|
||||
|
||||
:exclamation: If you get a poor framerate, it could be that the camera is switched to photo mode, which produces a low framerate (but a higher quality picture). This is because `bcm2835-v4l2` switches to photo mode at resolutions higher than `1280x720`. To work around this, pass the `max_video_width` and `max_video_height` module parameters like so:
|
||||
|
||||
```bash
|
||||
```
|
||||
$ modprobe bcm2835-v4l2 max_video_width=2592 max_video_height=1944
|
||||
```
|
||||
|
||||
-----
|
||||
# Integrations
|
||||
|
||||
## Janus
|
||||
µ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 {
|
||||
@@ -125,7 +209,7 @@ v4l2 utilities provide the tools to manage USB webcam setting and information. S
|
||||
|
||||
-----
|
||||
# License
|
||||
Copyright (C) 2018-2021 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
|
||||
|
||||
141
README.ru.md
141
README.ru.md
@@ -1,141 +0,0 @@
|
||||
# µStreamer
|
||||
[](https://github.com/pikvm/ustreamer/actions?query=workflow%3ACI)
|
||||
[](https://discord.gg/bpmXfz5)
|
||||
|
||||
[[English version]](README.md)
|
||||
|
||||
|
||||
µStreamer - это маленький и очень быстрый сервер, который позволяет организовать трансляцию видео в формате [MJPG](https://en.wikipedia.org/wiki/Motion_JPEG) с любого устройства V4L2 в сеть. Этот формат нативно поддерживается всеми современными браузерами и большинством приложений для просмотра видео (mplayer, VLC и так далее). µStreamer был разработан в рамках проекта [Pi-KVM](https://github.com/pikvm/pikvm) специально для стриминга с устройств видеозахвата [VGA](https://www.amazon.com/dp/B0126O0RDC) и [HDMI](https://auvidea.com/b101-hdmi-to-csi-2-bridge-15-pin-fpc/) с максимально возможным разрешением и FPS, которые только позволяет железо.
|
||||
|
||||
Функционально µStreamer очень похож на [mjpg-streamer](https://github.com/jacksonliam/mjpg-streamer) при использовании им плагинов ```input_uvc.so``` и ```output_http.so```, однако имеет ряд серьезных отличий. Основные приведены в этой таблице:
|
||||
|
||||
| **Фича** | **µStreamer** | **mjpg-streamer** |
|
||||
|----------|---------------|-------------------|
|
||||
| Многопоточное кодирование JPEG | ✔ | ✘ |
|
||||
| Аппаратное кодирование с помощью [OpenMAX IL](https://www.khronos.org/openmaxil) на Raspberry Pi | ✔ | ✘ |
|
||||
| Поведение при физическом отключении<br>устройства от сервера во время работы | ✔ Транслирует черный экран<br>с надписью ```NO SIGNAL```,<br>пока устройство не будет подключено снова | ✘ Прерывает трансляцию <sup>1</sup> |
|
||||
| Поддержка [DV-таймингов](https://linuxtv.org/downloads/v4l-dvb-apis-new/userspace-api/v4l/dv-timings.html) - возможности<br>изменения параметров разрешения<br>трансляции на лету по сигналу<br>источника (устройства видеозахвата) | ✔ | ☹ Условно есть <sup>1</sup> |
|
||||
| Возможность пропуска фреймов при передаче<br>статического изображения по HTTP<br>для экономии трафика | ✔ <sup>2</sup> | ✘ |
|
||||
| Стрим через UNIX domain socket | ✔ | ✘ |
|
||||
| Дебаг-логи без перекомпиляции,<br>логгирование статистики производительности,<br>возможность получения параметров<br>трансляции по HTTP | ✔ | ✘ |
|
||||
| Возможность сервить файлы встроенным<br>HTTP-сервером | ✔ | ☹ Нет каталогов |
|
||||
| Вывод сигналов о состоянии стрима на GPIO<br>с помощью [libgpiod](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/about) | ✔ | ✘ |
|
||||
| Поддержка контролов веб-камер (фокус,<br> движение сервами) и всяких настроек,<br> типа яркости, через HTTP | ✘ | ✔ |
|
||||
| Совместимость с API mjpg-streamer'а | ✔ | :) |
|
||||
|
||||
Сносочки:
|
||||
* ```1``` Еще до написания µStreamer, я запилил [патч](https://github.com/jacksonliam/mjpg-streamer/pull/164), добавляющий в mjpg-streamer поддержку DV-таймингов и предотвращающий его зависание при отключении устройства. Однако патч, увы, далек от совершенства и я не гарантирую его стопроцентную работоспособность, поскольку код mjpg-streamer чрезвычайно запутан и очень плохо структурирован. Учитывая это, а также то, что в дальнейшем мне потребовались многопоточность и аппаратное кодирование JPEG, было принято решение написать свой стрим-сервер с нуля, чтобы не тратить силы на поддержку лишнего легаси.
|
||||
|
||||
* ```2``` Это фича позволяет в несколько раз снизить объем исходящего трафика при трансляции HDMI, однако немного увеличивает загрузку процессора. Суть в том, что HDMI - полностью цифровой интерфейс, и новый захваченный фрейм может быть идентичен предыдущему в точности до байта. В этом случае нет нужды передавать одну и ту же картинку по сети несколько раз в секунду. При использовании опции `--drop-same-frames=20`, µStreamer будет дропать все одинаковые фреймы, но не более 20 подряд. Новый фрейм сравнивается с предыдущим сначала по длине, а затем помощью ```memcmp()```.
|
||||
|
||||
-----
|
||||
# TL;DR
|
||||
Если вам нужно вещать стрим с уличной камеры и управлять ее параметрами - возьмите mjpg-streamer. Если же вам нужно очень качественное изображение с высоким FPS - µStreamer ваш бро.
|
||||
|
||||
-----
|
||||
# Сборка
|
||||
Для сборки вам понадобятся ```make```, ```gcc```, ```libevent``` с поддержкой ```pthreads```, ```libjpeg8```/```libjpeg-turbo``` и ```libbsd``` (только для Linux).
|
||||
|
||||
* Arch: `sudo pacman -S libevent libjpeg-turbo libutil-linux libbsd`.
|
||||
* Raspbian: `sudo apt install libevent-dev libjpeg8-dev libbsd-dev`. Добавьте `libraspberrypi-dev` для сборки с `WITH_OMX=1` и `libgpiod` для `WITH_GPIO=1`.
|
||||
* Debian/Ubuntu: `sudo apt install build-essential libevent-dev libjpeg-dev libbsd-dev`.
|
||||
|
||||
На Raspberry Pi программу можно собрать с поддержкой OpenMAX IL. Для этого передайте ```make``` параметр ```WITH_OMX=1```. Для включения сборки с поддержкой GPIO установите [libgpiod](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/about) и добавьте параметр ```WITH_GPIO=1```. Если при сборке компилятор ругается на отсутствие функции ```pthread_get_name_np()``` или другой подобной, добавьте параметр ```WITH_PTHREAD_NP=0``` (по умолчанию он включен). При аналогичной ошибке с функцией ```setproctitle()``` добавьте параметр ```WITH_SETPROCTITLE=0```.
|
||||
|
||||
```
|
||||
$ git clone --depth=1 https://github.com/pikvm/ustreamer
|
||||
$ cd ustreamer
|
||||
$ make
|
||||
$ ./ustreamer --help
|
||||
```
|
||||
|
||||
Для Arch Linux в AUR есть готовый пакет: https://aur.archlinux.org/packages/ustreamer. На Raspberry Pi програма автоматически собирается с поддержкой OpenMAX IL, если обнаружит нужные хедеры в ```/opt/vc/include```.
|
||||
Порт для FreeBSD: https://www.freshports.org/multimedia/ustreamer.
|
||||
|
||||
-----
|
||||
# Использование
|
||||
Будучи запущенным без аргументов, ```ustreamer``` попробует открыть устройство ```/dev/video0``` с разрешением 640x480 и начать трансляцию на ```http://127.0.0.1:8080```. Это поведение может быть изменено с помощью опций ```--device```, ```--host``` и ```--port```. Пример вещания на всю сеть по 80-м порту:
|
||||
```
|
||||
# ./ustreamer --device=/dev/video1 --host=0.0.0.0 --port=80
|
||||
```
|
||||
|
||||
:exclamation: Обратите внимание, что начиная с версии µStreamer v2.0 кросс-доменные запросы были выключены по умолчанию [по соображениям безопасности](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). Чтобы включить старое поведение, используйте опцию `--allow-origin=\*`.
|
||||
|
||||
Рекомендуемый способ запуска µStreamer для работы с [Auvidea B101](https://www.raspberrypi.org/forums/viewtopic.php?f=38&t=120702&start=400#p1339178) на Raspberry Pi:
|
||||
```bash
|
||||
$ ./ustreamer \
|
||||
--format=uyvy \ # Настройка входного формата устройства
|
||||
--encoder=omx \ # Использование аппаратного кодирования с помощью OpenMAX
|
||||
--workers=3 \ # Максимум воркеров для OpenMAX
|
||||
--persistent \ # Не переинициализировать устройство при таймауте (например, когда был отключен HDMI-кабель)
|
||||
--dv-timings \ # Включение DV-таймингов
|
||||
--drop-same-frames=30 # Экономим трафик
|
||||
```
|
||||
|
||||
:exclamation: Обратите внимание, что для использования `--drop-same-frames` для разных браузеров нужно использовать ряд специальных параметров в `/stream` (за деталями обратитесь к урлу `/`).
|
||||
|
||||
За полным списком опций обращайтесь ко встроенной справке: ```ustreamer --help```.
|
||||
|
||||
-----
|
||||
# Камера Raspberry Pi
|
||||
Пример использования камеры Raspberry Pi v1:
|
||||
```bash
|
||||
$ sudo modprobe bcm2835-v4l2
|
||||
$ ./ustreamer --host :: -m jpeg --device-timeout=5 --buffers=3 -r 2592x1944
|
||||
```
|
||||
|
||||
:exclamation: Обратите внимание что боле новые модели камеры имеют другое максимальное разрешение. Список поддерживаемых разрешений можно найти в [документации PiCamera](https://picamera.readthedocs.io/en/release-1.13/fov.html#sensor-modes).
|
||||
|
||||
:exclamation: Если камера выдает низкий фреймрейт, возможно что она работает в фото-режиме, где производит более низкий фпс, но более качественную кратинку. Это происходит потому что `bcm2835-v4l2` переключает камеру в фото-режим на разрешениях выше `1280x720`. Чтобы обойти это, передайте параметры `max_video_width` и `max_video_height` при загрузке модуля, например:
|
||||
|
||||
```bash
|
||||
$ modprobe bcm2835-v4l2 max_video_width=2592 max_video_height=1944
|
||||
```
|
||||
|
||||
-----
|
||||
# Интеграция
|
||||
|
||||
## Nginx
|
||||
Если uStreamer находится на Nginx, то последний будет буферизировать поток и создавать дополнительную задержку в стриме. Чтобы задержки не было, буферизацию можно отключить:
|
||||
|
||||
```nginx
|
||||
location /stream {
|
||||
postpone_output 0;
|
||||
proxy_buffering off;
|
||||
proxy_ignore_headers X-Accel-Buffering;
|
||||
proxy_pass http://ustreamer;
|
||||
}
|
||||
```
|
||||
|
||||
-----
|
||||
# Утилиты V4L2
|
||||
V4L2 предоставляет ряд официальных утилит для управления USB-вебкамерами и получения информации об устройствах. С их помощью можно писать всякие настроечные скрипты и запускать их по крону, если, например, вам требуется изменять настройки экспозиции в зависимости от времени суток. Пакет с этими утилитами доступен на всех дистрибутивах Linux и обычно называется `v4l-utils`.
|
||||
|
||||
* Вывести список видеоустройств: `v4l2-ctl --list-devices`.
|
||||
* Вывести список доступных контролов устройства: `v4l2-ctl -d /dev/video0 --list-ctrls`.
|
||||
* Вывести список доступных форматов видео: `v4l2-ctl -d /dev/video0 --list-formats-ext`.
|
||||
* Показать текущее значение контрола: `v4l2-ctl -d /dev/video0 --get-ctrl=exposure_auto`.
|
||||
* Изменить значение контрола: `v4l2-ctl -d /dev/video0 --set-ctrl=exposure_auto=1`.
|
||||
|
||||
Больше примеров вы можете найти [здесь](https://www.kurokesu.com/main/2016/01/16/manual-usb-camera-settings-in-linux/), а документацию в [`man v4l2-ctl`](https://www.mankier.com/1/v4l2-ctl).
|
||||
|
||||
-----
|
||||
# Смотрите также
|
||||
* [Запуск с помощью systemd-сервиса](https://github.com/pikvm/ustreamer/issues/16).
|
||||
|
||||
-----
|
||||
# Лицензия
|
||||
Copyright (C) 2018-2021 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
|
||||
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/.
|
||||
237
docs/h264.md
Normal file
237
docs/h264.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# Streaming H.264 Video over WebRTC with Janus
|
||||
|
||||
µStreamer supports bandwidth-efficient streaming using [H.264 compression](https://en.wikipedia.org/wiki/Advanced_Video_Coding) and the Janus WebRTC server.
|
||||
|
||||
This guide explains how to configure µStreamer to provide an H.264 video stream and consume it within a web application using WebRTC.
|
||||
|
||||
## Components
|
||||
|
||||
In addition to µStreamer, H.264 streaming involves the following components:
|
||||
|
||||
- [**Janus**](https://janus.conf.meetecho.com/): A general-purpose WebRTC server.
|
||||
- [**µStreamer Janus Plugin**](https://github.com/pikvm/ustreamer/tree/master/janus): A Janus plugin that allows Janus to consume a video stream from µStreamer via shared memory.
|
||||
- [**Janus JavaScript Client**](https://janus.conf.meetecho.com/docs/JS.html): A frontend library that communicates with the Janus server and the µStreamer Janus plugin.
|
||||
|
||||
## Control Flow
|
||||
|
||||
To connect to a µStreamer video stream over WebRTC, an application must establish the following control flow:
|
||||
|
||||
1. The backend server starts the µStreamer service.
|
||||
1. The backend server starts the Janus WebRTC server with the µStreamer Janus plugin.
|
||||
1. The client-side JavaScript application establishes a connection to the Janus server.
|
||||
1. The client-side JavaScript application instructs the Janus server to attach the µStreamer Janus plugin and start the video stream.
|
||||
1. The client-side JavaScript application renders the video stream in the web browser.
|
||||
|
||||
## Server Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
To integrate with Janus, µStreamer has the following prerequisites:
|
||||
|
||||
- The system packages that µStreamer depends on (see the [main build instructions](../README.md#building)).
|
||||
- The Janus WebRTC server with WebSockets transport enabled (see the [Janus documentation](https://github.com/meetecho/janus-gateway)).
|
||||
|
||||
### Fixing Janus C Headers
|
||||
|
||||
To compile µStreamer with the Janus option, (see [“Installation”](#installation)), you need to make the Janus header files available within µStreamer's build context.
|
||||
|
||||
First, create a symbolic link from `/usr/include` to the Janus install directory. By default, Janus installs to `/opt/janus`.
|
||||
|
||||
```sh
|
||||
ln -s /opt/janus/include/janus /usr/include/janus
|
||||
```
|
||||
|
||||
Janus uses `#include` paths that make it difficult for third-party libraries to build against its headers. To fix this, modify the `#include` directive in `janus/plugins/plugin.h` to prepend a `../` to the included file name (`#include "refcount.h"` → `#include "../refcount.h"`).
|
||||
|
||||
```sh
|
||||
sed \
|
||||
--in-place \
|
||||
--expression 's|^#include "refcount.h"$|#include "../refcount.h"|g' \
|
||||
/usr/include/janus/plugins/plugin.h
|
||||
```
|
||||
|
||||
### Installation
|
||||
|
||||
First, compile µStreamer with the Janus plugin option (`WITH_JANUS`).
|
||||
|
||||
```sh
|
||||
git clone --depth=1 https://github.com/pikvm/ustreamer
|
||||
cd ustreamer
|
||||
make WITH_JANUS=1
|
||||
```
|
||||
|
||||
The `WITH_JANUS` option compiles the µStreamer Janus plugin and outputs it to `janus/libjanus_ustreamer.so`. Move this file to the plugin directory of your Janus installation.
|
||||
|
||||
```sh
|
||||
mv \
|
||||
janus/libjanus_ustreamer.so \
|
||||
/opt/janus/lib/janus/plugins/libjanus_ustreamer.so
|
||||
```
|
||||
|
||||
Finally, specify a qualifier for the shared memory object so that the µStreamer Janus plugin can read µStreamer's video data.
|
||||
|
||||
```sh
|
||||
cat > /opt/janus/lib/janus/configs/janus.plugin.ustreamer.jcfg <<EOF
|
||||
memsink: {
|
||||
object = "demo::ustreamer::h264"
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
If you're using a TC358743-based video capture device that supports audio capture, run the following command to enable audio streaming:
|
||||
|
||||
```sh
|
||||
cat << EOF >> /opt/janus/lib/janus/configs/janus.plugin.ustreamer.jcfg
|
||||
audio: {
|
||||
device = "hw:1"
|
||||
tc358743 = "/dev/video0"
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
### Start µStreamer and the Janus WebRTC Server
|
||||
|
||||
For µStreamer to share the video stream with the µStreamer Janus plugin, µStreamer must run with the following command-line flags:
|
||||
|
||||
- `--h264-sink` with the qualifier of the shared memory object you specified above (`demo::ustreamer::h264`)
|
||||
- `--h264-sink-mode` with the permissions bitmask for the shared memory object (e.g., `660`)
|
||||
- `--h264-sink-rm` to clean up the shared memory object when the µStreamer process exits
|
||||
|
||||
To load the µStreamer Janus plugin and configuration, the Janus WebRTC server must run with the following command-line flags:
|
||||
|
||||
- `--configs-folder` with the path to the Janus configuration directory (e.g., `/opt/janus/lib/janus/configs/`)
|
||||
- `--plugins-folder` with the path to the Janus plugin directory (e.g., `/opt/janus/lib/janus/plugins/`)
|
||||
|
||||
## Client Setup
|
||||
|
||||
Once an application's backend server is running µStreamer and Janus, a browser-based JavaScript application can consume the server's video stream.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
The client needs to load the following JavaScript libraries:
|
||||
|
||||
- [WebRTC Adapter library](https://webrtc.github.io/adapter/adapter-8.1.0.js) (`webrtc-adapter.js`, version `8.1.0`)
|
||||
- [Janus Gateway JavaScript library](https://raw.githubusercontent.com/meetecho/janus-gateway/v1.0.0/html/janus.js) (`janus.js`, version `1.0.0`)
|
||||
|
||||
### Control Flow
|
||||
|
||||
The client-side JavaScript application uses the following control flow:
|
||||
|
||||
1. The client loads and initializes the Janus client library.
|
||||
1. The client establishes a WebSockets connection to the Janus server.
|
||||
1. The client instructs the Janus server to attach the µStreamer Janus plugin.
|
||||
1. On success, the client obtains a plugin handle through which it can send requests directly to the µStreamer Janus plugin. The client processes responses via the `attach` callbacks:
|
||||
- `onmessage` for general messages
|
||||
- `onremotetrack` for the H.264 video stream and (optionally) an Opus audio stream
|
||||
1. The client issues a `watch` request to the µStreamer Janus plugin, which initiates the H.264 stream in the plugin itself.
|
||||
- It takes a few seconds for uStreamer's video stream to become available to Janus. The first `watch` request may fail, so the client must retry the `watch` request.
|
||||
1. The client and server negotiate the underlying parameters of the WebRTC session. This procedure is called JavaScript Session Establishment Protocol (JSEP). The server makes a `jsepOffer` to the client, and the client responds with a `jsepAnswer`.
|
||||
1. The client issues a `start` request to the µStreamer Janus plugin to indicate that the client wants to begin consuming the video stream.
|
||||
1. The µStreamer Janus plugin delivers the H.264 video stream and (optionally) an Opus audio stream to the client via WebRTC.
|
||||
1. The Janus client library invokes the `onremotetrack` callback with the video stream. The client attaches the video stream to the `<video>` element, rendering the video stream in the browser window.
|
||||
1. (if an audio track is available) The Janus client library invokes the `onremotetrack` callback with the Opus audio stream. The client adds the audio stream to the `<video>` element, rendering the audio in the browser window.
|
||||
|
||||
### Sample Code
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>µStreamer H.264 demo</title>
|
||||
<script src="https://webrtc.github.io/adapter/adapter-8.1.0.js"></script>
|
||||
<!-- janus.js is the JavaScript client library of Janus, as specified above in
|
||||
the prerequisites section of the client setup. You might need to change
|
||||
the `src` path, depending on where you serve this file from. -->
|
||||
<script src="janus.js"></script>
|
||||
<style>
|
||||
video {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<video id="webrtc-output" autoplay playsinline muted></video>
|
||||
<script type="text/javascript">
|
||||
// Initialize Janus library.
|
||||
Janus.init({
|
||||
// Turn on debug logs in the browser console.
|
||||
debug: true,
|
||||
|
||||
// Configure Janus to use standard browser APIs internally.
|
||||
dependencies: Janus.useDefaultDependencies(),
|
||||
});
|
||||
|
||||
// Establish a WebSockets connection to the server.
|
||||
const janus = new Janus({
|
||||
// Specify the URL of the Janus server’s WebSockets endpoint.
|
||||
server: `ws://${window.location.hostname}:8188/`,
|
||||
|
||||
// Callback function if the client connects successfully.
|
||||
success: attachUStreamerPlugin,
|
||||
|
||||
// Callback function if the client fails to connect.
|
||||
error: console.error,
|
||||
});
|
||||
|
||||
let uStreamerPluginHandle = null;
|
||||
|
||||
function attachUStreamerPlugin() {
|
||||
// Instruct the server to attach the µStreamer Janus plugin.
|
||||
janus.attach({
|
||||
// Qualifier of the plugin.
|
||||
plugin: "janus.plugin.ustreamer",
|
||||
|
||||
// Callback function, for when the server attached the plugin
|
||||
// successfully.
|
||||
success: function (pluginHandle) {
|
||||
uStreamerPluginHandle = pluginHandle;
|
||||
// Instruct the µStreamer Janus plugin to initiate streaming.
|
||||
uStreamerPluginHandle.send({ message: { request: "watch", params: {audio: true} } });
|
||||
},
|
||||
|
||||
// Callback function if the server fails to attach the plugin.
|
||||
error: console.error,
|
||||
|
||||
// Callback function for processing messages from the Janus server.
|
||||
onmessage: function (msg, jsepOffer) {
|
||||
// If there is a JSEP offer, respond to it. This starts the WebRTC
|
||||
// connection.
|
||||
if (jsepOffer) {
|
||||
uStreamerPluginHandle.createAnswer({
|
||||
jsep: jsepOffer,
|
||||
// Prevent the client from sending audio and video, as this would
|
||||
// trigger a permission dialog in the browser.
|
||||
media: { audioSend: false, videoSend: false },
|
||||
success: function (jsepAnswer) {
|
||||
uStreamerPluginHandle.send({
|
||||
message: { request: "start" },
|
||||
jsep: jsepAnswer,
|
||||
});
|
||||
},
|
||||
error: console.error,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Callback function, for when a media stream arrives.
|
||||
onremotetrack: function (mediaStreamTrack, mediaId, isAdded) {
|
||||
if (isAdded) {
|
||||
// Attach the received media track to the video element. Cloning the
|
||||
// mediaStreamTrack creates a new object with a distinct, globally
|
||||
// unique stream identifier.
|
||||
const videoElement = document.getElementById("webrtc-output");
|
||||
if (videoElement.srcObject === null) {
|
||||
videoElement.srcObject = new MediaStream();
|
||||
}
|
||||
videoElement.srcObject.addTrack(mediaStreamTrack.clone());
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
50
docs/ssl/README.md
Normal file
50
docs/ssl/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Adding SSL
|
||||
These days, browsers are not happy if you have HTTP content on an HTTPS page.
|
||||
The browser will not show an HTTP stream on a page if the parent page is from a site which is using HTTPS.
|
||||
|
||||
The files in this folder configure an Nginx proxy in front of the µStreamer stream.
|
||||
Using certbot, an SSL cert is created from Let's Encrypt and installed.
|
||||
These scripts can be modified to add SSL to just about any HTTP server.
|
||||
|
||||
The scripts are not fire and forget.
|
||||
They will require some pre-configuration and are interactive (you'll be asked questions while they're running).
|
||||
They have been tested using the following setup.
|
||||
1. A Raspberry Pi 4
|
||||
1. µStreamer set up and running as a service
|
||||
1. Internally on port 8080
|
||||
1. Public port will be 5101
|
||||
1. Verizon home Wi-Fi router
|
||||
1. Domain registration from GoDaddy
|
||||
|
||||
## The Script
|
||||
Below is an overview of the steps performed by `ssl-config.sh` (for Raspberry OS):
|
||||
1. Install snapd - certbot uses this for installation
|
||||
1. Install certbot
|
||||
1. Get a free cert from Let's Encrypt using certbot
|
||||
1. Install nginx
|
||||
1. Configures nginx to proxy for µStreamer
|
||||
|
||||
## Steps
|
||||
1. Create a public DNS entry.
|
||||
1. Pointing to the Pi itself or the public IP of the router behind which the Pi sits.
|
||||
1. This would be managed in the domain registrar, such as GoDaddy.
|
||||
1. Use a subdomain, such as `webcam.domain.com`
|
||||
1. Port Forwarding
|
||||
1. If using a Wi-Fi router, create a port forwarding rule which passes traffic from port 80 to the Pi. This is needed for certbot to ensure your DNS entry reaches the Pi, even if your final port will be something else.
|
||||
1. Create a second rule for your final setup. For example, forward traffic from the router on port 5101 to the Pi's IP port 8080.
|
||||
1. Update the ustreamer-proxy file in this folder
|
||||
1. Replace `your.domain.com` with a fully qualified domain, it's three places in the proxy file.
|
||||
1. Modify the line `listen 5101 ssl` port if needed. This is the public port, not the port on which the µStreamer service is running
|
||||
1. Modify `proxy_pass http://127.0.0.1:8080;` with the working address of the internal µStreamer service.
|
||||
1. Run the script
|
||||
1. Stand buy, certbot asks some basic questions, such as email, domain, agree to terms, etc.
|
||||
1. `bash ssl-config.sh`
|
||||
1. Test your URL!
|
||||
|
||||
## Down the Road
|
||||
Two important points to keep in mind for the future:
|
||||
1. Dynamic IP - Most routers do not have a static IP address on the WAN side. So, if you reboot your router or if your internet provider gives you a new IP, you'll have to update the DNS entry.
|
||||
1. Many routers have some sort of dynamic DNS feature. This would automatically update the DNS entry for you. That functionality is outside the scope of this document.
|
||||
1. SSL Renewals - certbot automatically creates a task to renew the SSL cert before it expires. Assuming the Pi is running all the time, this shouldn't be an issue.
|
||||
|
||||
## Enjoy!
|
||||
20
docs/ssl/ssl-config.sh
Normal file
20
docs/ssl/ssl-config.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo -e "\e[32mInstalling snapd...\e[0m"
|
||||
sudo apt install snapd -y
|
||||
sudo snap install core
|
||||
|
||||
|
||||
echo -e "\e[32mInstalling certbot, don't leave, it's going to ask questions...\e[0m"
|
||||
sudo snap install --classic certbot
|
||||
sudo ln -s /snap/bin/certbot /usr/bin/certbot
|
||||
sudo certbot certonly --standalone
|
||||
sudo certbot renew --dry-run
|
||||
|
||||
|
||||
echo -e "\e[32mInstalling nginx...\e[0m"
|
||||
sudo apt-get install nginx -y
|
||||
sudo cp ustreamer-proxy /etc/nginx/sites-available/ustreamer-proxy
|
||||
sudo ln -s /etc/nginx/sites-available/ustreamer-proxy /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
13
docs/ssl/ustreamer-proxy
Normal file
13
docs/ssl/ustreamer-proxy
Normal file
@@ -0,0 +1,13 @@
|
||||
server {
|
||||
listen 5101 ssl;
|
||||
server_name your.domain.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/your.domain.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/your.domain.com/privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080; # Change this to the uStreamer server address
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
97
docs/youtube.md
Normal file
97
docs/youtube.md
Normal file
@@ -0,0 +1,97 @@
|
||||
## Streaming to third party services
|
||||
|
||||
This method provides the ability of streaming a USB Webcam and include audio to large audiences.
|
||||
It uses to two machines. One is a Raspberry Pi and the other a more capable machine to performance
|
||||
the encoding of the video and audio that is streamed to the third party service such as YouTube.
|
||||
|
||||
Another benefit of using a browser (http stream) is the video can have overlays add in the custom ustreamer webpage.
|
||||
For example a cron process that retrieves weather information and updates a file to include on the page, announcements,
|
||||
or other creative ideas. The audio stream can also be something other than the webcam mic (music, voice files, etc.)
|
||||
and easily changed on the second machine setup. In the following example filtering is applied in ffmpeg to
|
||||
improve the sound of the webcam mic making vocals clearer and more intelligible.
|
||||
|
||||
* Machine 1:
|
||||
* USB webcam on the machine (Pi for example) running ustreamer (video) and VLC (audio). Remember to make any needed firewall changes if machine 2 is on a separate network so it can reach the ports for the video and audio.
|
||||
* To stream audio from the Pi.
|
||||
```
|
||||
/usr/bin/vlc -I dummy -vvv alsa://hw:2,0 --sout #transcode{acodec=mp3,ab=128}:standard{access=http,mux=ts,dst=:[PickAPort}
|
||||
```
|
||||
|
||||
* Machine 2:
|
||||
* On a more capable box run the video stream in a browser using ffmpeg to combine the video (browser) and audio and stream to YouTube or other services. In this example a VM with two virtual monitors running the browser full screen one of the monitors is used.
|
||||
|
||||
Script to stream the combination to YouTube:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
KEY=$1
|
||||
echo
|
||||
echo Cleanup -------------------------------------------------
|
||||
source live-yt.key
|
||||
killall -9 ffmpeg
|
||||
killall -9 chromium
|
||||
sleep 3
|
||||
|
||||
echo Setup General--------------------------------------------
|
||||
cd /home/[USER]
|
||||
rm -f nohup.out
|
||||
export DISPLAY=:0.0
|
||||
export $(dbus-launch)
|
||||
|
||||
echo Setup Chromium-------------------------------------------
|
||||
CHROMIUM_TEMP=/home/{USER]/tmp/chromium
|
||||
rm -rf $CHROMIUM_TEMP.bak
|
||||
mv $CHROMIUM_TEMP $CHROMIUM_TEMP.bak
|
||||
mkdir -p $CHROMIUM_TEMP
|
||||
|
||||
echo Start Chromium ------------------------------------------
|
||||
nohup /usr/lib/chromium/chromium \
|
||||
--new-window "http://[ustreamerURL]" \
|
||||
--start-fullscreen \
|
||||
--disable \
|
||||
--disable-translate \
|
||||
--disable-infobars \
|
||||
--disable-suggestions-service \
|
||||
--disable-save-password-bubble \
|
||||
--disable-new-tab-first-run \
|
||||
--disable-session-crashed-bubble \
|
||||
--disable-bundled-ppapi-flash \
|
||||
--disable-gpu \
|
||||
--enable-javascript \
|
||||
--enable-user-scripts \
|
||||
--disk-cache-dir=$CHROMIUM_TEMP/cache/ustreamer/ \
|
||||
--user-data-dir=$CHROMIUM_TEMP/user_data/ustreamer/ \
|
||||
--window-position=1440,12 \
|
||||
>/dev/null 2>&1 &
|
||||
sleep 5
|
||||
|
||||
echo Start FFMpeg---------------------------------------------
|
||||
nohup /usr/bin/ffmpeg \
|
||||
-loglevel level+warning \
|
||||
-thread_queue_size 512 \
|
||||
-framerate 30 \
|
||||
-f x11grab \
|
||||
-s 1920x1080 \
|
||||
-probesize 42M \
|
||||
-i :0.0+1024,0 \
|
||||
-i http://[VLCaudioURL] \
|
||||
-filter:a "volume=10, highpass=f=300, lowpass=f=2800" \
|
||||
-c:v libx264 \
|
||||
-pix_fmt yuv420p \
|
||||
-g 60 \
|
||||
-b:v 2500k \
|
||||
-c:a libmp3lame \
|
||||
-ar 44100 \
|
||||
-b:a 32k \
|
||||
-preset ultrafast \
|
||||
-maxrate 5000k \
|
||||
-bufsize 2500k \
|
||||
-preset ultrafast \
|
||||
-flvflags no_duration_filesize \
|
||||
-f flv "rtmp://a.rtmp.youtube.com/live2/$KEY" \
|
||||
>/home/{USER]/ff-audio.log 2>&1 &
|
||||
|
||||
echo Done ----------------------------------------------------
|
||||
echo
|
||||
```
|
||||
|
||||
*PS: Recipe by David Klippel*
|
||||
@@ -1,4 +1,4 @@
|
||||
DESTDIR ?=
|
||||
R_DESTDIR ?=
|
||||
PREFIX ?= /usr/local
|
||||
|
||||
CC ?= gcc
|
||||
@@ -9,10 +9,10 @@ LDFLAGS ?=
|
||||
# =====
|
||||
_PLUGIN = libjanus_ustreamer.so
|
||||
|
||||
_CFLAGS = -fPIC -MD -c -std=c11 -Wall -Wextra -D_GNU_SOURCE $(shell pkg-config --cflags glib-2.0) $(CFLAGS)
|
||||
_LDFLAGS = -shared -lm -pthread -lrt -ljansson $(shell pkg-config --libs glib-2.0) $(LDFLAGS)
|
||||
_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)
|
||||
|
||||
_SRCS = $(shell ls src/*.c)
|
||||
_SRCS = $(shell ls src/uslibs/*.c src/*.c)
|
||||
|
||||
_BUILD = build
|
||||
|
||||
@@ -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:
|
||||
|
||||
293
janus/src/audio.c
Normal file
293
janus/src/audio.c
Normal file
@@ -0,0 +1,293 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# 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 "audio.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdatomic.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include <pthread.h>
|
||||
#include <alsa/asoundlib.h>
|
||||
#include <speex/speex_resampler.h>
|
||||
#include <opus/opus.h>
|
||||
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/tools.h"
|
||||
#include "uslibs/array.h"
|
||||
#include "uslibs/ring.h"
|
||||
#include "uslibs/threading.h"
|
||||
|
||||
#include "logging.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(s16))
|
||||
|
||||
#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 {
|
||||
s16 data[_MAX_BUF16];
|
||||
} _pcm_buffer_s;
|
||||
|
||||
typedef struct {
|
||||
u8 data[_MAX_BUF8]; // Worst case
|
||||
uz used;
|
||||
u64 pts;
|
||||
} _enc_buffer_s;
|
||||
|
||||
|
||||
static _pcm_buffer_s *_pcm_buffer_init(void);
|
||||
static _enc_buffer_s *_enc_buffer_init(void);
|
||||
|
||||
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, uint pcm_hz) {
|
||||
us_audio_s *audio;
|
||||
US_CALLOC(audio, 1);
|
||||
audio->pcm_hz = pcm_hz;
|
||||
US_RING_INIT_WITH_ITEMS(audio->pcm_ring, 8, _pcm_buffer_init);
|
||||
US_RING_INIT_WITH_ITEMS(audio->enc_ring, 8, _enc_buffer_init);
|
||||
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_RING_DELETE_WITH_ITEMS(audio->enc_ring, free);
|
||||
US_RING_DELETE_WITH_ITEMS(audio->pcm_ring, free);
|
||||
if (audio->tids_created) {
|
||||
US_JLOG_INFO("audio", "Pipeline closed");
|
||||
}
|
||||
free(audio);
|
||||
}
|
||||
|
||||
int us_audio_get_encoded(us_audio_s *audio, u8 *data, uz *size, u64 *pts) {
|
||||
if (atomic_load(&audio->stop)) {
|
||||
return -1;
|
||||
}
|
||||
const int ri = us_ring_consumer_acquire(audio->enc_ring, 0.1);
|
||||
if (ri < 0) {
|
||||
return -2;
|
||||
}
|
||||
const _enc_buffer_s *const buf = audio->enc_ring->items[ri];
|
||||
if (*size < buf->used) {
|
||||
us_ring_consumer_release(audio->enc_ring, ri);
|
||||
return -3;
|
||||
}
|
||||
memcpy(data, buf->data, buf->used);
|
||||
*size = buf->used;
|
||||
*pts = buf->pts;
|
||||
us_ring_consumer_release(audio->enc_ring, ri);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static _pcm_buffer_s *_pcm_buffer_init(void) {
|
||||
_pcm_buffer_s *buf;
|
||||
US_CALLOC(buf, 1);
|
||||
return buf;
|
||||
}
|
||||
|
||||
static _enc_buffer_s *_enc_buffer_init(void) {
|
||||
_enc_buffer_s *buf;
|
||||
US_CALLOC(buf, 1);
|
||||
return buf;
|
||||
}
|
||||
|
||||
static void *_pcm_thread(void *v_audio) {
|
||||
US_THREAD_SETTLE("us_a_pcm");
|
||||
|
||||
us_audio_s *const audio = v_audio;
|
||||
u8 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;
|
||||
}
|
||||
|
||||
const int ri = us_ring_producer_acquire(audio->pcm_ring, 0);
|
||||
if (ri >= 0) {
|
||||
_pcm_buffer_s *const out = audio->pcm_ring->items[ri];
|
||||
memcpy(out->data, in, audio->pcm_size);
|
||||
us_ring_producer_release(audio->pcm_ring, ri);
|
||||
} else {
|
||||
US_JLOG_ERROR("audio", "PCM ring is full");
|
||||
}
|
||||
}
|
||||
|
||||
atomic_store(&audio->stop, true);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void *_encoder_thread(void *v_audio) {
|
||||
US_THREAD_SETTLE("us_a_enc");
|
||||
|
||||
us_audio_s *const audio = v_audio;
|
||||
s16 in_res[_MAX_BUF16];
|
||||
|
||||
while (!atomic_load(&audio->stop)) {
|
||||
const int in_ri = us_ring_consumer_acquire(audio->pcm_ring, 0.1);
|
||||
if (in_ri < 0) {
|
||||
continue;
|
||||
}
|
||||
_pcm_buffer_s *const in = audio->pcm_ring->items[in_ri];
|
||||
|
||||
s16 *in_ptr;
|
||||
if (audio->res != NULL) {
|
||||
assert(audio->pcm_hz != _ENCODER_INPUT_HZ);
|
||||
u32 in_count = audio->pcm_frames;
|
||||
u32 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;
|
||||
}
|
||||
|
||||
const int out_ri = us_ring_producer_acquire(audio->enc_ring, 0);
|
||||
if (out_ri < 0) {
|
||||
US_JLOG_ERROR("audio", "OPUS encoder queue is full");
|
||||
us_ring_consumer_release(audio->pcm_ring, in_ri);
|
||||
continue;
|
||||
}
|
||||
_enc_buffer_s *const out = audio->enc_ring->items[out_ri];
|
||||
|
||||
const int size = opus_encode(audio->enc, in_ptr, _HZ_TO_FRAMES(_ENCODER_INPUT_HZ), out->data, US_ARRAY_LEN(out->data));
|
||||
us_ring_consumer_release(audio->pcm_ring, in_ri);
|
||||
|
||||
if (size >= 0) {
|
||||
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);
|
||||
} else {
|
||||
_JLOG_PERROR_OPUS(size, "audio", "Fatal: Can't encode PCM frame to OPUS");
|
||||
}
|
||||
us_ring_producer_release(audio->enc_ring, out_ri);
|
||||
}
|
||||
|
||||
atomic_store(&audio->stop, true);
|
||||
return NULL;
|
||||
}
|
||||
61
janus/src/audio.h
Normal file
61
janus/src/audio.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 <stdatomic.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 {
|
||||
snd_pcm_t *pcm;
|
||||
uint pcm_hz;
|
||||
uint pcm_frames;
|
||||
uz pcm_size;
|
||||
snd_pcm_hw_params_t *pcm_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_audio_s;
|
||||
|
||||
|
||||
bool us_audio_probe(const char *name);
|
||||
|
||||
us_audio_s *us_audio_init(const char *name, uint pcm_hz);
|
||||
void us_audio_destroy(us_audio_s *audio);
|
||||
|
||||
int us_audio_get_encoded(us_audio_s *audio, u8 *data, uz *size, u64 *pts);
|
||||
@@ -1 +0,0 @@
|
||||
../../src/libs/base64.c
|
||||
@@ -1 +0,0 @@
|
||||
../../src/libs/base64.h
|
||||
158
janus/src/client.c
Normal file
158
janus/src/client.c
Normal file
@@ -0,0 +1,158 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# 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 "client.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdatomic.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <pthread.h>
|
||||
#include <janus/plugins/plugin.h>
|
||||
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/tools.h"
|
||||
#include "uslibs/threading.h"
|
||||
#include "uslibs/list.h"
|
||||
#include "uslibs/ring.h"
|
||||
|
||||
#include "logging.h"
|
||||
#include "rtp.h"
|
||||
|
||||
|
||||
static void *_video_thread(void *v_client);
|
||||
static void *_audio_thread(void *v_client);
|
||||
static void *_common_thread(void *v_client, bool video);
|
||||
|
||||
|
||||
us_janus_client_s *us_janus_client_init(janus_callbacks *gw, janus_plugin_session *session) {
|
||||
us_janus_client_s *client;
|
||||
US_CALLOC(client, 1);
|
||||
client->gw = gw;
|
||||
client->session = session;
|
||||
atomic_init(&client->transmit, false);
|
||||
atomic_init(&client->transmit_audio, false);
|
||||
atomic_init(&client->video_orient, 0);
|
||||
|
||||
atomic_init(&client->stop, false);
|
||||
|
||||
US_RING_INIT_WITH_ITEMS(client->video_ring, 2048, us_rtp_init);
|
||||
US_THREAD_CREATE(client->video_tid, _video_thread, client);
|
||||
|
||||
US_RING_INIT_WITH_ITEMS(client->audio_ring, 64, us_rtp_init);
|
||||
US_THREAD_CREATE(client->audio_tid, _audio_thread, client);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
void us_janus_client_destroy(us_janus_client_s *client) {
|
||||
atomic_store(&client->stop, true);
|
||||
|
||||
US_THREAD_JOIN(client->video_tid);
|
||||
US_RING_DELETE_WITH_ITEMS(client->video_ring, us_rtp_destroy);
|
||||
|
||||
US_THREAD_JOIN(client->audio_tid);
|
||||
US_RING_DELETE_WITH_ITEMS(client->audio_ring, us_rtp_destroy);
|
||||
|
||||
free(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))
|
||||
) {
|
||||
us_ring_s *const ring = (rtp->video ? client->video_ring : client->audio_ring);
|
||||
const int ri = us_ring_producer_acquire(ring, 0);
|
||||
if (ri < 0) {
|
||||
US_JLOG_ERROR("client", "Session %p %s ring is full",
|
||||
client->session, (rtp->video ? "video" : "audio"));
|
||||
return;
|
||||
}
|
||||
memcpy(ring->items[ri], rtp, sizeof(us_rtp_s));
|
||||
us_ring_producer_release(ring, ri);
|
||||
}
|
||||
}
|
||||
|
||||
static void *_video_thread(void *v_client) {
|
||||
US_THREAD_SETTLE("us_c_video");
|
||||
return _common_thread(v_client, true);
|
||||
}
|
||||
|
||||
static void *_audio_thread(void *v_client) {
|
||||
US_THREAD_SETTLE("us_c_audio");
|
||||
return _common_thread(v_client, false);
|
||||
}
|
||||
|
||||
static void *_common_thread(void *v_client, bool video) {
|
||||
us_janus_client_s *const client = v_client;
|
||||
us_ring_s *const ring = (video ? client->video_ring : client->audio_ring);
|
||||
assert(ring != NULL); // Audio may be NULL
|
||||
|
||||
while (!atomic_load(&client->stop)) {
|
||||
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 = {
|
||||
.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.
|
||||
.mindex = (rtp.video ? 0 : 1),
|
||||
# endif
|
||||
};
|
||||
janus_plugin_rtp_extensions_reset(&packet.extensions);
|
||||
|
||||
/*if (rtp->zero_playout_delay) {
|
||||
// https://github.com/pikvm/pikvm/issues/784
|
||||
packet.extensions.min_delay = 0;
|
||||
packet.extensions.max_delay = 0;
|
||||
} else {
|
||||
packet.extensions.min_delay = 0;
|
||||
// 10s - Chromium/WebRTC default
|
||||
// 3s - Firefox default
|
||||
packet.extensions.max_delay = 300; // == 3s, i.e. 10ms granularity
|
||||
}*/
|
||||
|
||||
if (rtp.video) {
|
||||
const uint video_orient = atomic_load(&client->video_orient);
|
||||
if (video_orient != 0) {
|
||||
packet.extensions.video_rotation = video_orient;
|
||||
}
|
||||
}
|
||||
|
||||
client->gw->relay_rtp(client->session, &packet);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
58
janus/src/client.h
Normal file
58
janus/src/client.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# 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 <pthread.h>
|
||||
#include <janus/plugins/plugin.h>
|
||||
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/list.h"
|
||||
#include "uslibs/ring.h"
|
||||
|
||||
#include "rtp.h"
|
||||
|
||||
|
||||
typedef struct us_janus_client_sx {
|
||||
janus_callbacks *gw;
|
||||
janus_plugin_session *session;
|
||||
atomic_bool transmit;
|
||||
atomic_bool transmit_audio;
|
||||
atomic_uint video_orient;
|
||||
|
||||
pthread_t video_tid;
|
||||
pthread_t audio_tid;
|
||||
atomic_bool stop;
|
||||
|
||||
us_ring_s *video_ring;
|
||||
us_ring_s *audio_ring;
|
||||
|
||||
US_LIST_STRUCT(struct us_janus_client_sx);
|
||||
} us_janus_client_s;
|
||||
|
||||
|
||||
us_janus_client_s *us_janus_client_init(janus_callbacks *gw, janus_plugin_session *session);
|
||||
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);
|
||||
107
janus/src/config.c
Normal file
107
janus/src/config.c
Normal file
@@ -0,0 +1,107 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# 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 "config.h"
|
||||
|
||||
#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"
|
||||
|
||||
|
||||
static char *_get_value(janus_config *jcfg, const char *section, const char *option);
|
||||
// static bool _get_bool(janus_config *jcfg, const char *section, const char *option, bool def);
|
||||
|
||||
|
||||
us_config_s *us_config_init(const char *config_dir_path) {
|
||||
us_config_s *config;
|
||||
US_CALLOC(config, 1);
|
||||
|
||||
char *config_file_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);
|
||||
|
||||
jcfg = janus_config_parse(config_file_path);
|
||||
if (jcfg == NULL) {
|
||||
US_JLOG_ERROR("config", "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)");
|
||||
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");
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
goto ok;
|
||||
|
||||
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->tc358743_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') {
|
||||
return NULL;
|
||||
}
|
||||
return us_strdup(option_obj->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;
|
||||
if (tmp != NULL) {
|
||||
value = (!strcasecmp(tmp, "1") || !strcasecmp(tmp, "true") || !strcasecmp(tmp, "yes"));
|
||||
free(tmp);
|
||||
}
|
||||
return value;
|
||||
}*/
|
||||
@@ -1 +0,0 @@
|
||||
../../src/libs/config.h
|
||||
35
janus/src/config.h
Normal file
35
janus/src/config.h
Normal file
@@ -0,0 +1,35 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# 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
|
||||
|
||||
|
||||
typedef struct {
|
||||
char *video_sink_name;
|
||||
|
||||
char *audio_dev_name;
|
||||
char *tc358743_dev_path;
|
||||
} us_config_s;
|
||||
|
||||
|
||||
us_config_s *us_config_init(const char *config_dir_path);
|
||||
void us_config_destroy(us_config_s *config);
|
||||
26
janus/src/const.h
Normal file
26
janus/src/const.h
Normal file
@@ -0,0 +1,26 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# 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_PLUGIN_NAME "ustreamer"
|
||||
#define US_PLUGIN_PACKAGE "janus.plugin.ustreamer"
|
||||
@@ -1 +0,0 @@
|
||||
../../src/libs/frame.c
|
||||
@@ -1 +0,0 @@
|
||||
../../src/libs/frame.h
|
||||
@@ -1 +0,0 @@
|
||||
../../src/libs/list.h
|
||||
38
janus/src/logging.h
Normal file
38
janus/src/logging.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# 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 "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); \
|
||||
}
|
||||
79
janus/src/memsinkfd.c
Normal file
79
janus/src/memsinkfd.c
Normal file
@@ -0,0 +1,79 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# 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 "memsinkfd.h"
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/tools.h"
|
||||
#include "uslibs/frame.h"
|
||||
#include "uslibs/memsinksh.h"
|
||||
|
||||
#include "logging.h"
|
||||
|
||||
|
||||
int us_memsink_fd_wait_frame(int fd, us_memsink_shared_s *mem, u64 last_id) {
|
||||
const ldf deadline_ts = us_get_now_monotonic() + 1; // wait_timeout
|
||||
ldf now_ts;
|
||||
do {
|
||||
const int result = us_flock_timedwait_monotonic(fd, 1); // lock_timeout
|
||||
now_ts = 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_ts < deadline_ts);
|
||||
return -2;
|
||||
}
|
||||
|
||||
int us_memsink_fd_get_frame(int fd, us_memsink_shared_s *mem, us_frame_s *frame, u64 *frame_id, bool key_required) {
|
||||
us_frame_set_data(frame, us_memsink_get_data(mem), 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 retval = 0;
|
||||
if (frame->format != V4L2_PIX_FMT_H264) {
|
||||
US_JLOG_ERROR("video", "Got non-H264 frame from memsink");
|
||||
retval = -1;
|
||||
}
|
||||
if (flock(fd, LOCK_UN) < 0) {
|
||||
US_JLOG_PERROR("video", "Can't unlock memsink");
|
||||
retval = -1;
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPG-HTTP streamer. #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2021 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,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define VERSION_MAJOR 4
|
||||
#define VERSION_MINOR 10
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/frame.h"
|
||||
#include "uslibs/memsinksh.h"
|
||||
|
||||
#define MAKE_VERSION2(_major, _minor) #_major "." #_minor
|
||||
#define MAKE_VERSION1(_major, _minor) MAKE_VERSION2(_major, _minor)
|
||||
#define VERSION MAKE_VERSION1(VERSION_MAJOR, VERSION_MINOR)
|
||||
|
||||
#define VERSION_U ((unsigned)(VERSION_MAJOR * 1000 + VERSION_MINOR))
|
||||
int us_memsink_fd_wait_frame(int fd, us_memsink_shared_s *mem, u64 last_id);
|
||||
int us_memsink_fd_get_frame(int fd, us_memsink_shared_s *mem, us_frame_s *frame, u64 *frame_id, bool key_required);
|
||||
@@ -1 +0,0 @@
|
||||
../../src/libs/memsinksh.h
|
||||
@@ -1,8 +1,8 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPG-HTTP streamer. #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2021 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,287 +20,260 @@
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdlib.h>
|
||||
#include <inttypes.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#include <pthread.h>
|
||||
#include <jansson.h>
|
||||
#include <janus/config.h>
|
||||
#include <janus/plugins/plugin.h>
|
||||
#include <janus/rtcp.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "tools.h"
|
||||
#include "threading.h"
|
||||
#include "list.h"
|
||||
#include "memsinksh.h"
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/const.h"
|
||||
#include "uslibs/tools.h"
|
||||
#include "uslibs/threading.h"
|
||||
#include "uslibs/list.h"
|
||||
#include "uslibs/ring.h"
|
||||
#include "uslibs/memsinksh.h"
|
||||
#include "uslibs/tc358743.h"
|
||||
|
||||
#include "const.h"
|
||||
#include "logging.h"
|
||||
#include "client.h"
|
||||
#include "audio.h"
|
||||
#include "rtp.h"
|
||||
#include "rtpv.h"
|
||||
#include "rtpa.h"
|
||||
#include "memsinkfd.h"
|
||||
#include "config.h"
|
||||
|
||||
|
||||
static int _plugin_init(janus_callbacks *gw, const char *config_file_path);
|
||||
static void _plugin_destroy(void);
|
||||
static us_config_s *_g_config = NULL;
|
||||
static const useconds_t _g_watchers_polling = 100000;
|
||||
|
||||
static void _plugin_create_session(janus_plugin_session *session, int *error);
|
||||
static void _plugin_destroy_session(janus_plugin_session *session, int *error);
|
||||
static json_t *_plugin_query_session(janus_plugin_session *session);
|
||||
static us_janus_client_s *_g_clients = NULL;
|
||||
static janus_callbacks *_g_gw = NULL;
|
||||
static us_ring_s *_g_video_ring = NULL;
|
||||
static us_rtpv_s *_g_rtpv = NULL;
|
||||
static us_rtpa_s *_g_rtpa = NULL;
|
||||
|
||||
static void _plugin_setup_media(janus_plugin_session *session);
|
||||
static void _plugin_hangup_media(janus_plugin_session *session);
|
||||
static pthread_t _g_video_rtp_tid;
|
||||
static atomic_bool _g_video_rtp_tid_created = false;
|
||||
static pthread_t _g_video_sink_tid;
|
||||
static atomic_bool _g_video_sink_tid_created = false;
|
||||
static pthread_t _g_audio_tid;
|
||||
static atomic_bool _g_audio_tid_created = false;
|
||||
|
||||
static struct janus_plugin_result *_plugin_handle_message(
|
||||
janus_plugin_session *session, char *transaction, json_t *msg, json_t *jsep);
|
||||
|
||||
static int _plugin_get_api_compatibility(void);
|
||||
static int _plugin_get_version(void);
|
||||
static const char *_plugin_get_version_string(void);
|
||||
static const char *_plugin_get_description(void);
|
||||
static const char *_plugin_get_name(void);
|
||||
static const char *_plugin_get_author(void);
|
||||
static const char *_plugin_get_package(void);
|
||||
static pthread_mutex_t _g_video_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_mutex_t _g_audio_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static atomic_bool _g_ready = false;
|
||||
static atomic_bool _g_stop = false;
|
||||
static atomic_bool _g_has_watchers = false;
|
||||
static atomic_bool _g_has_listeners = false;
|
||||
static atomic_bool _g_key_required = false;
|
||||
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Woverride-init"
|
||||
static janus_plugin _plugin = JANUS_PLUGIN_INIT(
|
||||
.init = _plugin_init,
|
||||
.destroy = _plugin_destroy,
|
||||
#define _LOCK_VIDEO US_MUTEX_LOCK(_g_video_lock)
|
||||
#define _UNLOCK_VIDEO US_MUTEX_UNLOCK(_g_video_lock)
|
||||
|
||||
.create_session = _plugin_create_session,
|
||||
.destroy_session = _plugin_destroy_session,
|
||||
.query_session = _plugin_query_session,
|
||||
#define _LOCK_AUDIO US_MUTEX_LOCK(_g_audio_lock)
|
||||
#define _UNLOCK_AUDIO US_MUTEX_UNLOCK(_g_audio_lock)
|
||||
|
||||
.setup_media = _plugin_setup_media,
|
||||
.hangup_media = _plugin_hangup_media,
|
||||
#define _LOCK_ALL { _LOCK_VIDEO; _LOCK_AUDIO; }
|
||||
#define _UNLOCK_ALL { _UNLOCK_AUDIO; _UNLOCK_VIDEO; }
|
||||
|
||||
.handle_message = _plugin_handle_message,
|
||||
|
||||
.get_api_compatibility = _plugin_get_api_compatibility,
|
||||
.get_version = _plugin_get_version,
|
||||
.get_version_string = _plugin_get_version_string,
|
||||
.get_description = _plugin_get_description,
|
||||
.get_name = _plugin_get_name,
|
||||
.get_author = _plugin_get_author,
|
||||
.get_package = _plugin_get_package,
|
||||
);
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
janus_plugin *create(void) { // cppcheck-suppress unusedFunction
|
||||
return &_plugin;
|
||||
}
|
||||
#define _READY atomic_load(&_g_ready)
|
||||
#define _STOP atomic_load(&_g_stop)
|
||||
#define _HAS_WATCHERS atomic_load(&_g_has_watchers)
|
||||
#define _HAS_LISTENERS atomic_load(&_g_has_listeners)
|
||||
|
||||
|
||||
typedef struct _client_sx {
|
||||
janus_plugin_session *session;
|
||||
bool transmit;
|
||||
|
||||
LIST_STRUCT(struct _client_sx);
|
||||
} _client_s;
|
||||
janus_plugin *create(void);
|
||||
|
||||
|
||||
static char *_g_memsink_obj = NULL;
|
||||
const long double _g_wait_timeout = 1;
|
||||
const long double _g_lock_timeout = 1;
|
||||
const useconds_t _g_lock_polling = 1000;
|
||||
const useconds_t _g_watchers_polling = 100000;
|
||||
static void *_video_rtp_thread(void *arg) {
|
||||
(void)arg;
|
||||
US_THREAD_SETTLE("us_video_rtp");
|
||||
atomic_store(&_g_video_rtp_tid_created, true);
|
||||
|
||||
static _client_s *_g_clients = NULL;
|
||||
static janus_callbacks *_g_gw = NULL;
|
||||
static rtp_s *_g_rtp = NULL;
|
||||
|
||||
static pthread_t _g_tid;
|
||||
static pthread_mutex_t _g_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static atomic_bool _g_ready = ATOMIC_VAR_INIT(false);
|
||||
static atomic_bool _g_stop = ATOMIC_VAR_INIT(false);
|
||||
static atomic_bool _g_has_watchers = ATOMIC_VAR_INIT(false);
|
||||
|
||||
|
||||
#define JLOG_INFO(_msg, ...) JANUS_LOG(LOG_INFO, "== %s -- " _msg "\n", _plugin_get_name(), ##__VA_ARGS__)
|
||||
#define JLOG_WARN(_msg, ...) JANUS_LOG(LOG_WARN, "== %s -- " _msg "\n", _plugin_get_name(), ##__VA_ARGS__)
|
||||
#define JLOG_ERROR(_msg, ...) JANUS_LOG(LOG_ERR, "== %s -- " _msg "\n", _plugin_get_name(), ##__VA_ARGS__)
|
||||
|
||||
#define JLOG_PERROR(_msg, ...) { \
|
||||
char _perror_buf[1024] = {0}; \
|
||||
char *_perror_ptr = errno_to_string(errno, _perror_buf, 1023); \
|
||||
JANUS_LOG(LOG_ERR, "[%s] " _msg ": %s\n", _plugin_get_name(), ##__VA_ARGS__, _perror_ptr); \
|
||||
}
|
||||
|
||||
#define LOCK A_MUTEX_LOCK(&_g_lock)
|
||||
#define UNLOCK A_MUTEX_UNLOCK(&_g_lock)
|
||||
#define READY atomic_load(&_g_ready)
|
||||
#define STOP atomic_load(&_g_stop)
|
||||
#define HAS_WATCHERS atomic_load(&_g_has_watchers)
|
||||
|
||||
|
||||
static void _relay_rtp_clients(const uint8_t *datagram, size_t size) {
|
||||
janus_plugin_rtp packet = {
|
||||
.video = true,
|
||||
.buffer = (char *)datagram,
|
||||
.length = size,
|
||||
};
|
||||
janus_plugin_rtp_extensions_reset(&packet.extensions);
|
||||
LIST_ITERATE(_g_clients, client, {
|
||||
if (client->transmit) {
|
||||
_g_gw->relay_rtp(client->session, &packet);
|
||||
while (!_STOP) {
|
||||
const int ri = us_ring_consumer_acquire(_g_video_ring, 0.1);
|
||||
if (ri >= 0) {
|
||||
const us_frame_s *const frame = _g_video_ring->items[ri];
|
||||
_LOCK_VIDEO;
|
||||
const bool zero_playout_delay = (frame->gop == 0);
|
||||
us_rtpv_wrap(_g_rtpv, frame, zero_playout_delay);
|
||||
_UNLOCK_VIDEO;
|
||||
us_ring_consumer_release(_g_video_ring, ri);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static int _wait_frame(int fd, memsink_shared_s* mem, uint64_t last_id) {
|
||||
long double deadline_ts = get_now_monotonic() + _g_wait_timeout;
|
||||
long double now;
|
||||
do {
|
||||
int retval = flock_timedwait_monotonic(fd, _g_lock_timeout);
|
||||
now = get_now_monotonic();
|
||||
if (retval < 0 && errno != EWOULDBLOCK) {
|
||||
JLOG_PERROR("Can't lock memsink");
|
||||
return -1;
|
||||
} else if (retval == 0) {
|
||||
if (mem->magic == MEMSINK_MAGIC && mem->version == MEMSINK_VERSION && mem->id != last_id) {
|
||||
return 0;
|
||||
}
|
||||
if (flock(fd, LOCK_UN) < 0) {
|
||||
JLOG_PERROR("Can't unlock memsink");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
usleep(_g_lock_polling);
|
||||
} while (now < deadline_ts);
|
||||
return -2;
|
||||
}
|
||||
|
||||
static int _get_frame(int fd, memsink_shared_s *mem, frame_s *frame, uint64_t *frame_id) {
|
||||
frame_set_data(frame, mem->data, mem->used);
|
||||
FRAME_COPY_META(mem, frame);
|
||||
*frame_id = mem->id;
|
||||
mem->last_client_ts = get_now_monotonic();
|
||||
int retval = 0;
|
||||
if (frame->format != V4L2_PIX_FMT_H264) {
|
||||
JLOG_ERROR("Got non-H264 frame from memsink");
|
||||
retval = -1;
|
||||
}
|
||||
if (flock(fd, LOCK_UN) < 0) {
|
||||
JLOG_PERROR("Can't unlock memsink");
|
||||
retval = -1;
|
||||
}
|
||||
return retval;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void *_clients_thread(UNUSED void *arg) {
|
||||
A_THREAD_RENAME("us_clients");
|
||||
atomic_store(&_g_ready, true);
|
||||
static void *_video_sink_thread(void *arg) {
|
||||
(void)arg;
|
||||
US_THREAD_SETTLE("us_video_sink");
|
||||
atomic_store(&_g_video_sink_tid_created, true);
|
||||
|
||||
frame_s *frame = frame_init();
|
||||
uint64_t frame_id = 0;
|
||||
us_frame_s *drop = us_frame_init();
|
||||
u64 frame_id = 0;
|
||||
int once = 0;
|
||||
|
||||
unsigned error_reported = 0;
|
||||
|
||||
# define IF_NOT_REPORTED(_error, ...) { \
|
||||
if (error_reported != _error) { __VA_ARGS__; error_reported = _error; } \
|
||||
}
|
||||
|
||||
while (!STOP) {
|
||||
if (!HAS_WATCHERS) {
|
||||
IF_NOT_REPORTED(1, {
|
||||
JLOG_INFO("No active watchers, memsink disconnected");
|
||||
});
|
||||
while (!_STOP) {
|
||||
if (!_HAS_WATCHERS) {
|
||||
US_ONCE({ US_JLOG_INFO("video", "No active watchers, memsink disconnected"); });
|
||||
usleep(_g_watchers_polling);
|
||||
continue;
|
||||
}
|
||||
|
||||
int fd = -1;
|
||||
memsink_shared_s *mem = NULL;
|
||||
us_memsink_shared_s *mem = NULL;
|
||||
|
||||
if ((fd = shm_open(_g_memsink_obj, O_RDWR, 0)) <= 0) {
|
||||
IF_NOT_REPORTED(2, {
|
||||
JLOG_PERROR("Can't open memsink");
|
||||
});
|
||||
const uz data_size = us_memsink_calculate_size(_g_config->video_sink_name);
|
||||
if (data_size == 0) {
|
||||
US_ONCE({ US_JLOG_ERROR("video", "Invalid memsink object suffix"); });
|
||||
goto close_memsink;
|
||||
}
|
||||
|
||||
if ((mem = memsink_shared_map(fd)) == NULL) {
|
||||
IF_NOT_REPORTED(3, {
|
||||
JLOG_PERROR("Can't map memsink");
|
||||
});
|
||||
if ((fd = shm_open(_g_config->video_sink_name, O_RDWR, 0)) <= 0) {
|
||||
US_ONCE({ US_JLOG_PERROR("video", "Can't open memsink"); });
|
||||
goto close_memsink;
|
||||
}
|
||||
|
||||
error_reported = 0;
|
||||
if ((mem = us_memsink_shared_map(fd, data_size)) == NULL) {
|
||||
US_ONCE({ US_JLOG_PERROR("video", "Can't map memsink"); });
|
||||
goto close_memsink;
|
||||
}
|
||||
|
||||
JLOG_INFO("Memsink opened; reading frames ...");
|
||||
while (!STOP && HAS_WATCHERS) {
|
||||
int result = _wait_frame(fd, mem, frame_id);
|
||||
if (result == 0) {
|
||||
if (_get_frame(fd, mem, frame, &frame_id) != 0) {
|
||||
once = 0;
|
||||
|
||||
US_JLOG_INFO("video", "Memsink opened; reading frames ...");
|
||||
while (!_STOP && _HAS_WATCHERS) {
|
||||
const int waited = us_memsink_fd_wait_frame(fd, mem, frame_id);
|
||||
if (waited == 0) {
|
||||
const int ri = us_ring_producer_acquire(_g_video_ring, 0);
|
||||
us_frame_s *frame;
|
||||
if (ri >= 0) {
|
||||
frame = _g_video_ring->items[ri];
|
||||
} else {
|
||||
US_ONCE({ US_JLOG_PERROR("video", "Video ring is full"); });
|
||||
frame = drop;
|
||||
}
|
||||
|
||||
const int got = us_memsink_fd_get_frame(fd, mem, frame, &frame_id, atomic_load(&_g_key_required));
|
||||
if (ri >= 0) {
|
||||
us_ring_producer_release(_g_video_ring, ri);
|
||||
}
|
||||
if (got < 0) {
|
||||
goto close_memsink;
|
||||
}
|
||||
LOCK;
|
||||
rtp_wrap_h264(_g_rtp, frame, _relay_rtp_clients);
|
||||
UNLOCK;
|
||||
} else if (result == -1) {
|
||||
|
||||
if (ri >= 0 && frame->key) {
|
||||
atomic_store(&_g_key_required, false);
|
||||
}
|
||||
} else if (waited != -2) {
|
||||
goto close_memsink;
|
||||
}
|
||||
}
|
||||
|
||||
close_memsink:
|
||||
if (mem != NULL) {
|
||||
JLOG_INFO("Memsink closed");
|
||||
memsink_shared_unmap(mem);
|
||||
mem = NULL;
|
||||
}
|
||||
if (fd > 0) {
|
||||
close(fd);
|
||||
fd = -1;
|
||||
}
|
||||
sleep(1); // error_delay
|
||||
close_memsink:
|
||||
if (mem != NULL) {
|
||||
us_memsink_shared_unmap(mem, data_size);
|
||||
mem = NULL;
|
||||
}
|
||||
US_CLOSE_FD(fd);
|
||||
US_JLOG_INFO("video", "Memsink closed");
|
||||
sleep(1); // error_delay
|
||||
}
|
||||
|
||||
# undef IF_NOT_REPORTED
|
||||
|
||||
frame_destroy(frame);
|
||||
us_frame_destroy(drop);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int _read_config(const char *config_dir_path) {
|
||||
char *config_file_path;
|
||||
janus_config *config = NULL;
|
||||
|
||||
A_ASPRINTF(config_file_path, "%s/%s.jcfg", config_dir_path, _plugin_get_package());
|
||||
JLOG_INFO("Reading config file '%s' ...", config_file_path);
|
||||
|
||||
config = janus_config_parse(config_file_path);
|
||||
if (config == NULL) {
|
||||
JLOG_ERROR("Can't read config");
|
||||
goto error;
|
||||
static int _check_tc358743_audio(uint *audio_hz) {
|
||||
int fd;
|
||||
if ((fd = open(_g_config->tc358743_dev_path, O_RDWR)) < 0) {
|
||||
US_JLOG_PERROR("audio", "Can't open TC358743 V4L2 device");
|
||||
return -1;
|
||||
}
|
||||
janus_config_print(config);
|
||||
|
||||
janus_config_category *config_memsink = janus_config_get_create(config, NULL, janus_config_type_category, "memsink");
|
||||
janus_config_item *config_memsink_obj = janus_config_get(config, config_memsink, janus_config_type_item, "object");
|
||||
if (config_memsink_obj == NULL || config_memsink_obj->value == NULL || config_memsink_obj->value[0] == '\0') {
|
||||
JLOG_ERROR("Missing config value: memsink.object");
|
||||
goto error;
|
||||
const int checked = us_tc358743_xioctl_get_audio_hz(fd, audio_hz);
|
||||
if (checked < 0) {
|
||||
US_JLOG_PERROR("audio", "Can't check TC358743 audio state (%d)", checked);
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
_g_memsink_obj = strdup(config_memsink_obj->value);
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int retval = 0;
|
||||
goto ok;
|
||||
error:
|
||||
retval = -1;
|
||||
ok:
|
||||
if (config) {
|
||||
janus_config_destroy(config);
|
||||
static void *_audio_thread(void *arg) {
|
||||
(void)arg;
|
||||
US_THREAD_SETTLE("us_audio");
|
||||
atomic_store(&_g_audio_tid_created, true);
|
||||
|
||||
assert(_g_config->audio_dev_name != NULL);
|
||||
assert(_g_config->tc358743_dev_path != NULL);
|
||||
|
||||
int once = 0;
|
||||
|
||||
while (!_STOP) {
|
||||
if (!_HAS_WATCHERS || !_HAS_LISTENERS) {
|
||||
usleep(_g_watchers_polling);
|
||||
continue;
|
||||
}
|
||||
free(config_file_path);
|
||||
return retval;
|
||||
|
||||
uint audio_hz = 0;
|
||||
us_audio_s *audio = NULL;
|
||||
|
||||
if (_check_tc358743_audio(&audio_hz) < 0) {
|
||||
goto close_audio;
|
||||
}
|
||||
if (audio_hz == 0) {
|
||||
US_ONCE({ US_JLOG_INFO("audio", "No audio presented from the host"); });
|
||||
goto close_audio;
|
||||
}
|
||||
US_ONCE({ US_JLOG_INFO("audio", "Detected host audio"); });
|
||||
if ((audio = us_audio_init(_g_config->audio_dev_name, audio_hz)) == NULL) {
|
||||
goto close_audio;
|
||||
}
|
||||
|
||||
once = 0;
|
||||
|
||||
while (!_STOP && _HAS_WATCHERS && _HAS_LISTENERS) {
|
||||
if (_check_tc358743_audio(&audio_hz) < 0 || audio->pcm_hz != audio_hz) {
|
||||
goto close_audio;
|
||||
}
|
||||
uz size = US_RTP_DATAGRAM_SIZE - US_RTP_HEADER_SIZE;
|
||||
u8 data[size];
|
||||
u64 pts;
|
||||
const int result = us_audio_get_encoded(audio, data, &size, &pts);
|
||||
if (result == 0) {
|
||||
_LOCK_AUDIO;
|
||||
us_rtpa_wrap(_g_rtpa, data, size, pts);
|
||||
_UNLOCK_AUDIO;
|
||||
} else if (result == -1) {
|
||||
goto close_audio;
|
||||
}
|
||||
}
|
||||
|
||||
close_audio:
|
||||
US_DELETE(audio, us_audio_destroy);
|
||||
sleep(1); // error_delay
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void _relay_rtp_clients(const us_rtp_s *rtp) {
|
||||
US_LIST_ITERATE(_g_clients, client, {
|
||||
us_janus_client_send(client, rtp);
|
||||
});
|
||||
}
|
||||
|
||||
static int _plugin_init(janus_callbacks *gw, const char *config_dir_path) {
|
||||
@@ -310,115 +283,121 @@ static int _plugin_init(janus_callbacks *gw, const char *config_dir_path) {
|
||||
// sysctl -w net.core.rmem_max=1000000
|
||||
// sysctl -w net.core.wmem_max=1000000
|
||||
|
||||
JLOG_INFO("Initializing plugin ...");
|
||||
assert(!READY);
|
||||
assert(!STOP);
|
||||
if (gw == NULL || config_dir_path == NULL || _read_config(config_dir_path) < 0) {
|
||||
US_JLOG_INFO("main", "Initializing PiKVM uStreamer plugin %s ...", US_VERSION);
|
||||
if (gw == NULL || config_dir_path == NULL || ((_g_config = us_config_init(config_dir_path)) == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
_g_gw = gw;
|
||||
_g_rtp = rtp_init();
|
||||
A_THREAD_CREATE(&_g_tid, _clients_thread, NULL);
|
||||
|
||||
US_RING_INIT_WITH_ITEMS(_g_video_ring, 64, us_frame_init);
|
||||
_g_rtpv = us_rtpv_init(_relay_rtp_clients);
|
||||
if (_g_config->audio_dev_name != NULL && us_audio_probe(_g_config->audio_dev_name)) {
|
||||
_g_rtpa = us_rtpa_init(_relay_rtp_clients);
|
||||
US_THREAD_CREATE(_g_audio_tid, _audio_thread, NULL);
|
||||
}
|
||||
US_THREAD_CREATE(_g_video_rtp_tid, _video_rtp_thread, NULL);
|
||||
US_THREAD_CREATE(_g_video_sink_tid, _video_sink_thread, NULL);
|
||||
|
||||
atomic_store(&_g_ready, true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void _plugin_destroy(void) {
|
||||
JLOG_INFO("Destroying plugin ...");
|
||||
US_JLOG_INFO("main", "Destroying plugin ...");
|
||||
|
||||
atomic_store(&_g_stop, true);
|
||||
if (READY) {
|
||||
A_THREAD_JOIN(_g_tid);
|
||||
}
|
||||
# define JOIN(_tid) { if (atomic_load(&_tid##_created)) { US_THREAD_JOIN(_tid); } }
|
||||
JOIN(_g_video_sink_tid);
|
||||
JOIN(_g_video_rtp_tid);
|
||||
JOIN(_g_audio_tid);
|
||||
# undef JOIN
|
||||
|
||||
LIST_ITERATE(_g_clients, client, {
|
||||
LIST_REMOVE(_g_clients, client);
|
||||
free(client);
|
||||
US_LIST_ITERATE(_g_clients, client, {
|
||||
US_LIST_REMOVE(_g_clients, client);
|
||||
us_janus_client_destroy(client);
|
||||
});
|
||||
_g_clients = NULL;
|
||||
|
||||
rtp_destroy(_g_rtp);
|
||||
_g_rtp = NULL;
|
||||
US_RING_DELETE_WITH_ITEMS(_g_video_ring, us_frame_destroy);
|
||||
|
||||
_g_gw = NULL;
|
||||
|
||||
if (_g_memsink_obj) {
|
||||
free(_g_memsink_obj);
|
||||
_g_memsink_obj = NULL;
|
||||
}
|
||||
US_DELETE(_g_rtpa, us_rtpa_destroy);
|
||||
US_DELETE(_g_rtpv, us_rtpv_destroy);
|
||||
US_DELETE(_g_config, us_config_destroy);
|
||||
}
|
||||
|
||||
#define IF_DISABLED(...) { if (!READY || STOP) { __VA_ARGS__ } }
|
||||
#define _IF_DISABLED(...) { if (!_READY || _STOP) { __VA_ARGS__ } }
|
||||
|
||||
static void _plugin_create_session(janus_plugin_session *session, int *error) {
|
||||
IF_DISABLED({ *error = -1; return; });
|
||||
LOCK;
|
||||
JLOG_INFO("Creating session %p ...", session);
|
||||
_client_s *client;
|
||||
A_CALLOC(client, 1);
|
||||
client->session = session;
|
||||
client->transmit = true;
|
||||
LIST_APPEND(_g_clients, client);
|
||||
static void _plugin_create_session(janus_plugin_session *session, int *err) {
|
||||
_IF_DISABLED({ *err = -1; return; });
|
||||
_LOCK_ALL;
|
||||
US_JLOG_INFO("main", "Creating session %p ...", session);
|
||||
us_janus_client_s *const client = us_janus_client_init(_g_gw, session);
|
||||
US_LIST_APPEND(_g_clients, client);
|
||||
atomic_store(&_g_has_watchers, true);
|
||||
UNLOCK;
|
||||
_UNLOCK_ALL;
|
||||
}
|
||||
|
||||
static void _plugin_destroy_session(janus_plugin_session* session, int *error) {
|
||||
IF_DISABLED({ *error = -1; return; });
|
||||
LOCK;
|
||||
static void _plugin_destroy_session(janus_plugin_session* session, int *err) {
|
||||
_IF_DISABLED({ *err = -1; return; });
|
||||
_LOCK_ALL;
|
||||
bool found = false;
|
||||
bool has_watchers = false;
|
||||
LIST_ITERATE(_g_clients, client, {
|
||||
bool has_listeners = false;
|
||||
US_LIST_ITERATE(_g_clients, client, {
|
||||
if (client->session == session) {
|
||||
JLOG_INFO("Removing session %p ...", session);
|
||||
LIST_REMOVE(_g_clients, client);
|
||||
free(client);
|
||||
US_JLOG_INFO("main", "Removing session %p ...", session);
|
||||
US_LIST_REMOVE(_g_clients, client);
|
||||
us_janus_client_destroy(client);
|
||||
found = true;
|
||||
} else {
|
||||
has_watchers = (has_watchers || client->transmit);
|
||||
has_watchers = (has_watchers || atomic_load(&client->transmit));
|
||||
has_listeners = (has_listeners || atomic_load(&client->transmit_audio));
|
||||
}
|
||||
});
|
||||
if (!found) {
|
||||
JLOG_WARN("No session %p", session);
|
||||
*error = -2;
|
||||
US_JLOG_WARN("main", "No session %p", session);
|
||||
*err = -2;
|
||||
}
|
||||
atomic_store(&_g_has_watchers, has_watchers);
|
||||
UNLOCK;
|
||||
atomic_store(&_g_has_listeners, has_listeners);
|
||||
_UNLOCK_ALL;
|
||||
}
|
||||
|
||||
static json_t *_plugin_query_session(janus_plugin_session *session) {
|
||||
IF_DISABLED({ return NULL; });
|
||||
_IF_DISABLED({ return NULL; });
|
||||
json_t *info = NULL;
|
||||
LOCK;
|
||||
LIST_ITERATE(_g_clients, client, {
|
||||
_LOCK_ALL;
|
||||
US_LIST_ITERATE(_g_clients, client, {
|
||||
if (client->session == session) {
|
||||
info = json_string("session_found");
|
||||
break;
|
||||
}
|
||||
});
|
||||
UNLOCK;
|
||||
_UNLOCK_ALL;
|
||||
return info;
|
||||
}
|
||||
|
||||
static void _set_transmit(janus_plugin_session *session, UNUSED const char *msg, bool transmit) {
|
||||
IF_DISABLED({ return; });
|
||||
LOCK;
|
||||
static void _set_transmit(janus_plugin_session *session, const char *msg, bool transmit) {
|
||||
(void)msg;
|
||||
_IF_DISABLED({ return; });
|
||||
_LOCK_ALL;
|
||||
bool found = false;
|
||||
bool has_watchers = false;
|
||||
LIST_ITERATE(_g_clients, client, {
|
||||
US_LIST_ITERATE(_g_clients, client, {
|
||||
if (client->session == session) {
|
||||
client->transmit = transmit;
|
||||
//JLOG_INFO("%s session %p", msg, session);
|
||||
atomic_store(&client->transmit, transmit);
|
||||
// US_JLOG_INFO("main", "%s session %p", msg, session);
|
||||
found = true;
|
||||
}
|
||||
has_watchers = (has_watchers || client->transmit);
|
||||
has_watchers = (has_watchers || atomic_load(&client->transmit));
|
||||
});
|
||||
if (!found) {
|
||||
JLOG_WARN("No session %p", session);
|
||||
US_JLOG_WARN("main", "No session %p", session);
|
||||
}
|
||||
atomic_store(&_g_has_watchers, has_watchers);
|
||||
UNLOCK;
|
||||
_UNLOCK_ALL;
|
||||
}
|
||||
|
||||
#undef IF_DISABLED
|
||||
#undef _IF_DISABLED
|
||||
|
||||
static void _plugin_setup_media(janus_plugin_session *session) { _set_transmit(session, "Unmuted", true); }
|
||||
static void _plugin_hangup_media(janus_plugin_session *session) { _set_transmit(session, "Muted", false); }
|
||||
@@ -429,8 +408,8 @@ static struct janus_plugin_result *_plugin_handle_message(
|
||||
assert(transaction != NULL);
|
||||
|
||||
# define FREE_MSG_JSEP { \
|
||||
if (msg) json_decref(msg); \
|
||||
if (jsep) json_decref(jsep); \
|
||||
US_DELETE(msg, json_decref); \
|
||||
US_DELETE(jsep, json_decref); \
|
||||
}
|
||||
|
||||
if (session == NULL || msg == NULL) {
|
||||
@@ -439,84 +418,184 @@ static struct janus_plugin_result *_plugin_handle_message(
|
||||
return janus_plugin_result_new(JANUS_PLUGIN_ERROR, (msg ? "No session" : "No message"), NULL);
|
||||
}
|
||||
|
||||
# define PUSH_ERROR(_error, _reason) { \
|
||||
/*JLOG_ERROR("Message error in session %p: %s", session, _reason);*/ \
|
||||
json_t *_event = json_object(); \
|
||||
json_object_set_new(_event, "ustreamer", json_string("event")); \
|
||||
json_object_set_new(_event, "error_code", json_integer(_error)); \
|
||||
json_object_set_new(_event, "error", json_string(_reason)); \
|
||||
_g_gw->push_event(session, &_plugin, transaction, _event, NULL); \
|
||||
json_decref(_event); \
|
||||
# define PUSH_ERROR(x_error, x_reason) { \
|
||||
/*US_JLOG_ERROR("main", "Message error in session %p: %s", session, x_reason);*/ \
|
||||
json_t *m_event = json_object(); \
|
||||
json_object_set_new(m_event, "ustreamer", json_string("event")); \
|
||||
json_object_set_new(m_event, "error_code", json_integer(x_error)); \
|
||||
json_object_set_new(m_event, "error", json_string(x_reason)); \
|
||||
_g_gw->push_event(session, create(), transaction, m_event, NULL); \
|
||||
json_decref(m_event); \
|
||||
}
|
||||
|
||||
json_t *request_obj = json_object_get(msg, "request");
|
||||
if (request_obj == NULL) {
|
||||
json_t *const request = json_object_get(msg, "request");
|
||||
if (request == NULL) {
|
||||
PUSH_ERROR(400, "Request missing");
|
||||
goto ok_wait;
|
||||
}
|
||||
|
||||
const char *request_str = json_string_value(request_obj);
|
||||
if (!request_str) {
|
||||
const char *const request_str = json_string_value(request);
|
||||
if (request_str == NULL) {
|
||||
PUSH_ERROR(400, "Request not a string");
|
||||
goto ok_wait;
|
||||
}
|
||||
//JLOG_INFO("Message: %s", request_str);
|
||||
// US_JLOG_INFO("main", "Message: %s", request_str);
|
||||
|
||||
# define PUSH_STATUS(_status, _jsep) { \
|
||||
json_t *_event = json_object(); \
|
||||
json_object_set_new(_event, "ustreamer", json_string("event")); \
|
||||
json_t *_result = json_object(); \
|
||||
json_object_set_new(_result, "status", json_string(_status)); \
|
||||
json_object_set_new(_event, "result", _result); \
|
||||
_g_gw->push_event(session, &_plugin, transaction, _event, _jsep); \
|
||||
json_decref(_event); \
|
||||
# define PUSH_STATUS(x_status, x_payload, x_jsep) { \
|
||||
json_t *const m_event = json_object(); \
|
||||
json_object_set_new(m_event, "ustreamer", json_string("event")); \
|
||||
json_t *const m_result = json_object(); \
|
||||
json_object_set_new(m_result, "status", json_string(x_status)); \
|
||||
if (x_payload != NULL) { \
|
||||
json_object_set_new(m_result, x_status, x_payload); \
|
||||
} \
|
||||
json_object_set_new(m_event, "result", m_result); \
|
||||
_g_gw->push_event(session, create(), transaction, m_event, x_jsep); \
|
||||
json_decref(m_event); \
|
||||
}
|
||||
|
||||
if (!strcmp(request_str, "start")) {
|
||||
PUSH_STATUS("started", NULL);
|
||||
PUSH_STATUS("started", NULL, NULL);
|
||||
|
||||
} else if (!strcmp(request_str, "stop")) {
|
||||
PUSH_STATUS("stopped", NULL);
|
||||
PUSH_STATUS("stopped", NULL, NULL);
|
||||
|
||||
} else if (!strcmp(request_str, "watch")) {
|
||||
char *sdp = rtp_make_sdp(_g_rtp);
|
||||
if (sdp == NULL) {
|
||||
PUSH_ERROR(503, "Haven't received SPS/PPS from memsink yet");
|
||||
goto ok_wait;
|
||||
bool with_audio = false;
|
||||
uint video_orient = 0;
|
||||
{
|
||||
json_t *const params = json_object_get(msg, "params");
|
||||
if (params != NULL) {
|
||||
{
|
||||
json_t *const obj = json_object_get(params, "audio");
|
||||
if (obj != NULL && json_is_boolean(obj)) {
|
||||
with_audio = (_g_rtpa != NULL && json_boolean_value(obj));
|
||||
}
|
||||
}
|
||||
{
|
||||
json_t *const obj = json_object_get(params, "orientation");
|
||||
if (obj != NULL && json_is_integer(obj)) {
|
||||
video_orient = json_integer_value(obj);
|
||||
switch (video_orient) {
|
||||
case 90: case 180: case 270: break;
|
||||
default: video_orient = 0; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//JLOG_INFO("SDP generated:\n%s", sdp);
|
||||
json_t *offer_jsep = json_pack("{ssss}", "type", "offer", "sdp", sdp);
|
||||
free(sdp);
|
||||
PUSH_STATUS("started", offer_jsep);
|
||||
json_decref(offer_jsep);
|
||||
|
||||
{
|
||||
char *sdp;
|
||||
char *const video_sdp = us_rtpv_make_sdp(_g_rtpv);
|
||||
char *const audio_sdp = (with_audio ? us_rtpa_make_sdp(_g_rtpa) : us_strdup(""));
|
||||
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
|
||||
);
|
||||
json_t *const offer_jsep = json_pack("{ssss}", "type", "offer", "sdp", sdp);
|
||||
PUSH_STATUS("started", NULL, offer_jsep);
|
||||
json_decref(offer_jsep);
|
||||
free(audio_sdp);
|
||||
free(video_sdp);
|
||||
free(sdp);
|
||||
}
|
||||
|
||||
{
|
||||
_LOCK_ALL;
|
||||
bool has_listeners = false;
|
||||
US_LIST_ITERATE(_g_clients, client, {
|
||||
if (client->session == session) {
|
||||
atomic_store(&client->transmit_audio, with_audio);
|
||||
atomic_store(&client->video_orient, video_orient);
|
||||
}
|
||||
has_listeners = (has_listeners || atomic_load(&client->transmit_audio));
|
||||
});
|
||||
atomic_store(&_g_has_listeners, has_listeners);
|
||||
_UNLOCK_ALL;
|
||||
}
|
||||
|
||||
} else if (!strcmp(request_str, "features")) {
|
||||
json_t *const features = json_pack("{sb}", "audio", (_g_rtpa != NULL));
|
||||
PUSH_STATUS("features", features, NULL);
|
||||
json_decref(features);
|
||||
|
||||
} else if (!strcmp(request_str, "key_required")) {
|
||||
// US_JLOG_INFO("main", "Got key_required message");
|
||||
atomic_store(&_g_key_required, true);
|
||||
|
||||
} else {
|
||||
PUSH_ERROR(405, "Not implemented");
|
||||
}
|
||||
|
||||
ok_wait:
|
||||
FREE_MSG_JSEP;
|
||||
return janus_plugin_result_new(JANUS_PLUGIN_OK_WAIT, NULL, NULL);
|
||||
ok_wait:
|
||||
FREE_MSG_JSEP;
|
||||
return janus_plugin_result_new(JANUS_PLUGIN_OK_WAIT, NULL, NULL);
|
||||
|
||||
# undef PUSH_STATUS
|
||||
# undef PUSH_ERROR
|
||||
# undef FREE_MSG_JSEP
|
||||
}
|
||||
|
||||
static void _plugin_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet) {
|
||||
(void)handle;
|
||||
(void)packet;
|
||||
if (packet->video && janus_rtcp_has_pli(packet->buffer, packet->length)) {
|
||||
// US_JLOG_INFO("main", "Got video PLI");
|
||||
atomic_store(&_g_key_required, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ***** Plugin *****
|
||||
|
||||
static int _plugin_get_api_compatibility(void) { return JANUS_PLUGIN_API_VERSION; }
|
||||
static int _plugin_get_version(void) { return VERSION_U; }
|
||||
static const char *_plugin_get_version_string(void) { return VERSION; }
|
||||
static const char *_plugin_get_description(void) { return "Pi-KVM uStreamer Janus plugin for H.264 video"; }
|
||||
static const char *_plugin_get_name(void) { return "ustreamer"; }
|
||||
static int _plugin_get_version(void) { return US_VERSION_U; }
|
||||
static const char *_plugin_get_version_string(void) { return US_VERSION; }
|
||||
static const char *_plugin_get_description(void) { return "PiKVM uStreamer Janus plugin for H.264 video"; }
|
||||
static const char *_plugin_get_name(void) { return US_PLUGIN_NAME; }
|
||||
static const char *_plugin_get_author(void) { return "Maxim Devaev <mdevaev@gmail.com>"; }
|
||||
static const char *_plugin_get_package(void) { return "janus.plugin.ustreamer"; }
|
||||
static const char *_plugin_get_package(void) { return US_PLUGIN_PACKAGE; }
|
||||
|
||||
janus_plugin *create(void) {
|
||||
# pragma GCC diagnostic push
|
||||
# pragma GCC diagnostic ignored "-Woverride-init"
|
||||
static janus_plugin plugin = JANUS_PLUGIN_INIT(
|
||||
.init = _plugin_init,
|
||||
.destroy = _plugin_destroy,
|
||||
|
||||
#undef STOP
|
||||
#undef READY
|
||||
#undef UNLOCK
|
||||
#undef LOCK
|
||||
.create_session = _plugin_create_session,
|
||||
.destroy_session = _plugin_destroy_session,
|
||||
.query_session = _plugin_query_session,
|
||||
|
||||
#undef JLOG_PERROR
|
||||
#undef JLOG_ERROR
|
||||
#undef JLOG_WARN
|
||||
#undef JLOG_INFO
|
||||
.setup_media = _plugin_setup_media,
|
||||
.hangup_media = _plugin_hangup_media,
|
||||
|
||||
.handle_message = _plugin_handle_message,
|
||||
|
||||
.get_api_compatibility = _plugin_get_api_compatibility,
|
||||
.get_version = _plugin_get_version,
|
||||
.get_version_string = _plugin_get_version_string,
|
||||
.get_description = _plugin_get_description,
|
||||
.get_name = _plugin_get_name,
|
||||
.get_author = _plugin_get_author,
|
||||
.get_package = _plugin_get_package,
|
||||
|
||||
.incoming_rtcp = _plugin_incoming_rtcp,
|
||||
);
|
||||
# pragma GCC diagnostic pop
|
||||
return &plugin;
|
||||
}
|
||||
|
||||
199
janus/src/rtp.c
199
janus/src/rtp.c
@@ -1,11 +1,11 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPG-HTTP streamer. #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# This source file is partially based on this code: #
|
||||
# - https://github.com/catid/kvm/blob/master/kvm_pipeline/src #
|
||||
# #
|
||||
# Copyright (C) 2018-2021 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# 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,204 +25,41 @@
|
||||
|
||||
#include "rtp.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#define PAYLOAD 96 // Payload type
|
||||
#define PRE 3 // Annex B prefix length
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/tools.h"
|
||||
|
||||
|
||||
void _rtp_process_nalu(rtp_s *rtp, const uint8_t *data, size_t size, uint32_t pts, bool marked, rtp_callback_f callback);
|
||||
static void _rtp_write_header(rtp_s *rtp, uint32_t pts, bool marked);
|
||||
|
||||
static ssize_t _find_annexb(const uint8_t *data, size_t size);
|
||||
|
||||
|
||||
rtp_s *rtp_init(void) {
|
||||
rtp_s *rtp;
|
||||
A_CALLOC(rtp, 1);
|
||||
rtp->ssrc = triple_u32(get_now_monotonic_u64());
|
||||
rtp->sps = frame_init();
|
||||
rtp->pps = frame_init();
|
||||
A_MUTEX_INIT(&rtp->mutex);
|
||||
us_rtp_s *us_rtp_init(void) {
|
||||
us_rtp_s *rtp;
|
||||
US_CALLOC(rtp, 1);
|
||||
return rtp;
|
||||
}
|
||||
|
||||
void rtp_destroy(rtp_s *rtp) {
|
||||
A_MUTEX_DESTROY(&rtp->mutex);
|
||||
frame_destroy(rtp->pps);
|
||||
frame_destroy(rtp->sps);
|
||||
void us_rtp_destroy(us_rtp_s *rtp) {
|
||||
free(rtp);
|
||||
}
|
||||
|
||||
char *rtp_make_sdp(rtp_s *rtp) {
|
||||
A_MUTEX_LOCK(&rtp->mutex);
|
||||
|
||||
if (rtp->sps->used == 0 || rtp->pps->used == 0) {
|
||||
A_MUTEX_UNLOCK(&rtp->mutex);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *sps = NULL;
|
||||
char *pps = NULL;
|
||||
base64_encode(rtp->sps->data, rtp->sps->used, &sps, NULL);
|
||||
base64_encode(rtp->pps->data, rtp->pps->used, &pps, NULL);
|
||||
|
||||
A_MUTEX_UNLOCK(&rtp->mutex);
|
||||
|
||||
// https://tools.ietf.org/html/rfc6184
|
||||
// https://github.com/meetecho/janus-gateway/issues/2443
|
||||
char *sdp;
|
||||
A_ASPRINTF(sdp,
|
||||
"v=0" RN
|
||||
"o=- %" PRIu64 " 1 IN IP4 127.0.0.1" RN
|
||||
"s=Pi-KVM uStreamer" RN
|
||||
"t=0 0" RN
|
||||
"m=video 1 RTP/SAVPF %d" RN
|
||||
"c=IN IP4 0.0.0.0" RN
|
||||
"a=rtpmap:%d H264/90000" RN
|
||||
"a=fmtp:%d profile-level-id=42E01F" RN
|
||||
"a=fmtp:%d packetization-mode=1" RN
|
||||
"a=fmtp:%d sprop-sps=%s" RN
|
||||
"a=fmtp:%d sprop-pps=%s" RN
|
||||
"a=rtcp-fb:%d nack" RN
|
||||
"a=rtcp-fb:%d nack pli" RN
|
||||
"a=rtcp-fb:%d goog-remb" RN
|
||||
"a=sendonly" RN,
|
||||
get_now_id() >> 1, PAYLOAD, PAYLOAD, PAYLOAD, PAYLOAD,
|
||||
PAYLOAD, sps,
|
||||
PAYLOAD, pps,
|
||||
PAYLOAD, PAYLOAD, PAYLOAD
|
||||
);
|
||||
|
||||
free(sps);
|
||||
free(pps);
|
||||
return sdp;
|
||||
void us_rtp_assign(us_rtp_s *rtp, uint payload, bool video) {
|
||||
rtp->payload = payload;
|
||||
rtp->video = video;
|
||||
rtp->ssrc = us_triple_u32(us_get_now_monotonic_u64());
|
||||
}
|
||||
|
||||
void rtp_wrap_h264(rtp_s *rtp, const frame_s *frame, rtp_callback_f callback) {
|
||||
// There is a complicated logic here but everything works as it should:
|
||||
// - https://github.com/pikvm/ustreamer/issues/115#issuecomment-893071775
|
||||
|
||||
assert(frame->format == V4L2_PIX_FMT_H264);
|
||||
|
||||
const uint32_t pts = get_now_monotonic_u64() * 9 / 100; // PTS units are in 90 kHz
|
||||
ssize_t last_offset = -PRE;
|
||||
|
||||
while (true) { // Find and iterate by nalus
|
||||
const size_t next_start = last_offset + PRE;
|
||||
ssize_t offset = _find_annexb(frame->data + next_start, frame->used - next_start);
|
||||
if (offset < 0) {
|
||||
break;
|
||||
}
|
||||
offset += next_start;
|
||||
|
||||
if (last_offset >= 0) {
|
||||
const uint8_t *data = frame->data + last_offset + PRE;
|
||||
size_t size = offset - last_offset - PRE;
|
||||
if (data[size - 1] == 0) { // Check for extra 00
|
||||
--size;
|
||||
}
|
||||
_rtp_process_nalu(rtp, data, size, pts, false, callback);
|
||||
}
|
||||
|
||||
last_offset = offset;
|
||||
}
|
||||
|
||||
if (last_offset >= 0) {
|
||||
const uint8_t *data = frame->data + last_offset + PRE;
|
||||
size_t size = frame->used - last_offset - PRE;
|
||||
_rtp_process_nalu(rtp, data, size, pts, true, callback);
|
||||
}
|
||||
}
|
||||
|
||||
void _rtp_process_nalu(rtp_s *rtp, const uint8_t *data, size_t size, uint32_t pts, bool marked, rtp_callback_f callback) {
|
||||
const unsigned ref_idc = (data[0] >> 5) & 3;
|
||||
const unsigned type = data[0] & 0x1F;
|
||||
|
||||
frame_s *ps = NULL;
|
||||
switch (type) {
|
||||
case 7: ps = rtp->sps; break;
|
||||
case 8: ps = rtp->pps; break;
|
||||
}
|
||||
if (ps) {
|
||||
A_MUTEX_LOCK(&rtp->mutex);
|
||||
frame_set_data(ps, data, size);
|
||||
A_MUTEX_UNLOCK(&rtp->mutex);
|
||||
}
|
||||
|
||||
# define HEADER_SIZE 12
|
||||
|
||||
if (size + HEADER_SIZE <= RTP_DATAGRAM_SIZE) {
|
||||
_rtp_write_header(rtp, pts, marked);
|
||||
memcpy(rtp->datagram + HEADER_SIZE, data, size);
|
||||
callback(rtp->datagram, size + HEADER_SIZE);
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t fu_overhead = HEADER_SIZE + 2; // FU-A overhead
|
||||
|
||||
const uint8_t *src = data + 1;
|
||||
ssize_t remaining = size - 1;
|
||||
|
||||
bool first = true;
|
||||
while (remaining > 0) {
|
||||
ssize_t frag_size = RTP_DATAGRAM_SIZE - fu_overhead;
|
||||
const bool last = (remaining <= frag_size);
|
||||
if (last) {
|
||||
frag_size = remaining;
|
||||
}
|
||||
|
||||
_rtp_write_header(rtp, pts, (marked && last));
|
||||
|
||||
rtp->datagram[HEADER_SIZE] = 28 | (ref_idc << 5);
|
||||
|
||||
uint8_t fu = type;
|
||||
if (first) {
|
||||
fu |= 0x80;
|
||||
}
|
||||
if (last) {
|
||||
fu |= 0x40;
|
||||
}
|
||||
rtp->datagram[HEADER_SIZE + 1] = fu;
|
||||
|
||||
memcpy(rtp->datagram + fu_overhead, src, frag_size);
|
||||
|
||||
callback(rtp->datagram, fu_overhead + frag_size);
|
||||
|
||||
src += frag_size;
|
||||
remaining -= frag_size;
|
||||
first = false;
|
||||
}
|
||||
|
||||
# undef HEADER_SIZE
|
||||
}
|
||||
|
||||
static void _rtp_write_header(rtp_s *rtp, uint32_t pts, bool marked) {
|
||||
uint32_t word0 = 0x80000000;
|
||||
void us_rtp_write_header(us_rtp_s *rtp, u32 pts, bool marked) {
|
||||
u32 word0 = 0x80000000;
|
||||
if (marked) {
|
||||
word0 |= 1 << 23;
|
||||
}
|
||||
word0 |= (PAYLOAD & 0x7F) << 16;
|
||||
word0 |= (rtp->payload & 0x7F) << 16;
|
||||
word0 |= rtp->seq;
|
||||
++rtp->seq;
|
||||
|
||||
# define WRITE_BE_U32(_offset, _value) *((uint32_t *)(rtp->datagram + _offset)) = __builtin_bswap32(_value)
|
||||
# define WRITE_BE_U32(x_offset, x_value) \
|
||||
*((u32*)(rtp->datagram + x_offset)) = __builtin_bswap32(x_value)
|
||||
WRITE_BE_U32(0, word0);
|
||||
WRITE_BE_U32(4, pts);
|
||||
WRITE_BE_U32(8, rtp->ssrc);
|
||||
# undef WRITE_BE_U32
|
||||
}
|
||||
|
||||
static ssize_t _find_annexb(const uint8_t *data, size_t size) {
|
||||
// Parses buffer for 00 00 01 start codes
|
||||
if (size >= PRE) {
|
||||
for (size_t index = 0; index <= size - PRE; ++index) {
|
||||
if (data[index] == 0 && data[index + 1] == 0 && data[index + 2] == 1) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
#undef PRE
|
||||
#undef PAYLOAD
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPG-HTTP streamer. #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# This source file is partially based on this code: #
|
||||
# - https://github.com/catid/kvm/blob/master/kvm_pipeline/src #
|
||||
# #
|
||||
# Copyright (C) 2018-2021 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# 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,45 +22,30 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <inttypes.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
#include "tools.h"
|
||||
#include "threading.h"
|
||||
#include "frame.h"
|
||||
#include "base64.h"
|
||||
#include "uslibs/types.h"
|
||||
|
||||
|
||||
// https://stackoverflow.com/questions/47635545/why-webrtc-chose-rtp-max-packet-size-to-1200-bytes
|
||||
#define RTP_DATAGRAM_SIZE 1200
|
||||
#define US_RTP_DATAGRAM_SIZE 1200
|
||||
#define US_RTP_HEADER_SIZE 12
|
||||
|
||||
|
||||
typedef struct {
|
||||
uint32_t ssrc;
|
||||
uint16_t seq;
|
||||
uint payload;
|
||||
bool video;
|
||||
u32 ssrc;
|
||||
|
||||
uint8_t datagram[RTP_DATAGRAM_SIZE];
|
||||
u16 seq;
|
||||
u8 datagram[US_RTP_DATAGRAM_SIZE];
|
||||
uz used;
|
||||
bool zero_playout_delay;
|
||||
} us_rtp_s;
|
||||
|
||||
frame_s *sps; // Actually not a frame, just a bytes storage
|
||||
frame_s *pps;
|
||||
|
||||
pthread_mutex_t mutex;
|
||||
} rtp_s;
|
||||
|
||||
typedef void (*rtp_callback_f)(const uint8_t *datagram, size_t size);
|
||||
typedef void (*us_rtp_callback_f)(const us_rtp_s *rtp);
|
||||
|
||||
|
||||
rtp_s *rtp_init(void);
|
||||
void rtp_destroy(rtp_s *rtp);
|
||||
us_rtp_s *us_rtp_init(void);
|
||||
void us_rtp_destroy(us_rtp_s *rtp);
|
||||
|
||||
char *rtp_make_sdp(rtp_s *rtp);
|
||||
void rtp_wrap_h264(rtp_s *rtp, const frame_s *frame, rtp_callback_f callback);
|
||||
void us_rtp_assign(us_rtp_s *rtp, uint payload, bool video);
|
||||
void us_rtp_write_header(us_rtp_s *rtp, u32 pts, bool marked);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPG-HTTP streamer. #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2021 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,54 +20,53 @@
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
#pragma once
|
||||
#include "rtpa.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <assert.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#include <IL/OMX_Core.h>
|
||||
#include <IL/OMX_Component.h>
|
||||
#include <IL/OMX_Broadcom.h>
|
||||
#include <interface/vcos/vcos_semaphore.h>
|
||||
|
||||
#include "../../../libs/tools.h"
|
||||
#include "../../../libs/logging.h"
|
||||
#include "../../../libs/frame.h"
|
||||
|
||||
#include "vcos.h"
|
||||
#include "formatters.h"
|
||||
#include "component.h"
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/tools.h"
|
||||
|
||||
|
||||
#ifndef CFG_OMX_MAX_ENCODERS
|
||||
# define CFG_OMX_MAX_ENCODERS 3 // Raspberry Pi limitation
|
||||
#endif
|
||||
#define OMX_MAX_ENCODERS ((unsigned)(CFG_OMX_MAX_ENCODERS))
|
||||
us_rtpa_s *us_rtpa_init(us_rtp_callback_f callback) {
|
||||
us_rtpa_s *rtpa;
|
||||
US_CALLOC(rtpa, 1);
|
||||
rtpa->rtp = us_rtp_init();
|
||||
us_rtp_assign(rtpa->rtp, 111, false);
|
||||
rtpa->callback = callback;
|
||||
return rtpa;
|
||||
}
|
||||
|
||||
void us_rtpa_destroy(us_rtpa_s *rtpa) {
|
||||
us_rtp_destroy(rtpa->rtp);
|
||||
free(rtpa);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
OMX_HANDLETYPE comp;
|
||||
OMX_BUFFERHEADERTYPE *input_buf;
|
||||
OMX_BUFFERHEADERTYPE *output_buf;
|
||||
bool input_required;
|
||||
bool output_available;
|
||||
bool failed;
|
||||
VCOS_SEMAPHORE_T handler_sem;
|
||||
char *us_rtpa_make_sdp(us_rtpa_s *rtpa) {
|
||||
const uint pl = 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,
|
||||
pl, pl, pl, pl, pl, // pl,
|
||||
rtpa->rtp->ssrc
|
||||
);
|
||||
return sdp;
|
||||
}
|
||||
|
||||
bool i_handler_sem;
|
||||
bool i_encoder;
|
||||
bool i_input_port_enabled;
|
||||
bool i_output_port_enabled;
|
||||
} omx_encoder_s;
|
||||
|
||||
|
||||
omx_encoder_s *omx_encoder_init(void);
|
||||
void omx_encoder_destroy(omx_encoder_s *omx);
|
||||
|
||||
int omx_encoder_prepare(omx_encoder_s *omx, const frame_s *frame, unsigned quality);
|
||||
int omx_encoder_compress(omx_encoder_s *omx, const frame_s *src, frame_s *dest);
|
||||
void us_rtpa_wrap(us_rtpa_s *rtpa, const u8 *data, uz size, u32 pts) {
|
||||
if (size + US_RTP_HEADER_SIZE <= US_RTP_DATAGRAM_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;
|
||||
rtpa->callback(rtpa->rtp);
|
||||
}
|
||||
}
|
||||
40
janus/src/rtpa.h
Normal file
40
janus/src/rtpa.h
Normal file
@@ -0,0 +1,40 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# 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"
|
||||
|
||||
|
||||
typedef struct {
|
||||
us_rtp_s *rtp;
|
||||
us_rtp_callback_f callback;
|
||||
} us_rtpa_s;
|
||||
|
||||
|
||||
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 u8 *data, uz size, u32 pts);
|
||||
184
janus/src/rtpv.c
Normal file
184
janus/src/rtpv.c
Normal file
@@ -0,0 +1,184 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# This source file is partially based on this code: #
|
||||
# - https://github.com/catid/kvm/blob/master/kvm_pipeline/src #
|
||||
# #
|
||||
# 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 "rtpv.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <inttypes.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/tools.h"
|
||||
#include "uslibs/frame.h"
|
||||
|
||||
|
||||
void _rtpv_process_nalu(us_rtpv_s *rtpv, const u8 *data, uz size, u32 pts, bool marked);
|
||||
|
||||
static sz _find_annexb(const u8 *data, uz size);
|
||||
|
||||
|
||||
us_rtpv_s *us_rtpv_init(us_rtp_callback_f callback) {
|
||||
us_rtpv_s *rtpv;
|
||||
US_CALLOC(rtpv, 1);
|
||||
rtpv->rtp = us_rtp_init();
|
||||
us_rtp_assign(rtpv->rtp, 96, true);
|
||||
rtpv->callback = callback;
|
||||
return rtpv;
|
||||
}
|
||||
|
||||
void us_rtpv_destroy(us_rtpv_s *rtpv) {
|
||||
us_rtp_destroy(rtpv->rtp);
|
||||
free(rtpv);
|
||||
}
|
||||
|
||||
char *us_rtpv_make_sdp(us_rtpv_s *rtpv) {
|
||||
// https://tools.ietf.org/html/rfc6184
|
||||
// https://github.com/meetecho/janus-gateway/issues/2443
|
||||
const uint pl = rtpv->rtp->payload;
|
||||
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=extmap:2 urn:3gpp:video-orientation" RN
|
||||
"a=sendonly" RN,
|
||||
pl, pl, pl, pl,
|
||||
pl, pl, pl,
|
||||
rtpv->rtp->ssrc
|
||||
);
|
||||
return sdp;
|
||||
}
|
||||
|
||||
#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
|
||||
|
||||
assert(frame->format == V4L2_PIX_FMT_H264);
|
||||
|
||||
rtpv->rtp->zero_playout_delay = zero_playout_delay;
|
||||
|
||||
const u32 pts = us_get_now_monotonic_u64() * 9 / 100; // PTS units are in 90 kHz
|
||||
sz last_offset = -_PRE;
|
||||
|
||||
while (true) { // Find and iterate by nalus
|
||||
const uz next_start = last_offset + _PRE;
|
||||
sz offset = _find_annexb(frame->data + next_start, frame->used - next_start);
|
||||
if (offset < 0) {
|
||||
break;
|
||||
}
|
||||
offset += next_start;
|
||||
|
||||
if (last_offset >= 0) {
|
||||
const u8 *const data = frame->data + last_offset + _PRE;
|
||||
uz 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 u8 *const data = frame->data + last_offset + _PRE;
|
||||
uz size = frame->used - last_offset - _PRE;
|
||||
_rtpv_process_nalu(rtpv, data, size, pts, true);
|
||||
}
|
||||
}
|
||||
|
||||
void _rtpv_process_nalu(us_rtpv_s *rtpv, const u8 *data, uz size, u32 pts, bool marked) {
|
||||
const uint ref_idc = (data[0] >> 5) & 3;
|
||||
const uint type = data[0] & 0x1F;
|
||||
u8 *dg = rtpv->rtp->datagram;
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
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_DATAGRAM_SIZE - fu_overhead;
|
||||
const bool last = (remaining <= frag_size);
|
||||
if (last) {
|
||||
frag_size = remaining;
|
||||
}
|
||||
|
||||
us_rtp_write_header(rtpv->rtp, pts, (marked && 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);
|
||||
rtpv->rtp->used = fu_overhead + frag_size;
|
||||
rtpv->callback(rtpv->rtp);
|
||||
|
||||
src += frag_size;
|
||||
remaining -= frag_size;
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
static sz _find_annexb(const u8 *data, uz size) {
|
||||
// Parses buffer for 00 00 01 start codes
|
||||
if (size >= _PRE) {
|
||||
for (uz index = 0; index <= size - _PRE; ++index) {
|
||||
if (data[index] == 0 && data[index + 1] == 0 && data[index + 2] == 1) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
#undef _PRE
|
||||
41
janus/src/rtpv.h
Normal file
41
janus/src/rtpv.h
Normal file
@@ -0,0 +1,41 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# 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 "uslibs/frame.h"
|
||||
|
||||
#include "rtp.h"
|
||||
|
||||
|
||||
typedef struct {
|
||||
us_rtp_s *rtp;
|
||||
us_rtp_callback_f callback;
|
||||
} us_rtpv_s;
|
||||
|
||||
|
||||
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);
|
||||
@@ -1 +0,0 @@
|
||||
../../src/libs/threading.h
|
||||
@@ -1 +0,0 @@
|
||||
../../src/libs/tools.h
|
||||
1
janus/src/uslibs/array.h
Symbolic link
1
janus/src/uslibs/array.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/array.h
|
||||
1
janus/src/uslibs/const.h
Symbolic link
1
janus/src/uslibs/const.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/const.h
|
||||
1
janus/src/uslibs/frame.c
Symbolic link
1
janus/src/uslibs/frame.c
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/frame.c
|
||||
1
janus/src/uslibs/frame.h
Symbolic link
1
janus/src/uslibs/frame.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/frame.h
|
||||
1
janus/src/uslibs/list.h
Symbolic link
1
janus/src/uslibs/list.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/list.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/memsinksh.h
Symbolic link
1
janus/src/uslibs/memsinksh.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/memsinksh.h
|
||||
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/tc358743.c
Symbolic link
1
janus/src/uslibs/tc358743.c
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/tc358743.c
|
||||
1
janus/src/uslibs/tc358743.h
Symbolic link
1
janus/src/uslibs/tc358743.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/tc358743.h
|
||||
1
janus/src/uslibs/threading.h
Symbolic link
1
janus/src/uslibs/threading.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/threading.h
|
||||
1
janus/src/uslibs/tools.h
Symbolic link
1
janus/src/uslibs/tools.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/tools.h
|
||||
1
janus/src/uslibs/types.h
Symbolic link
1
janus/src/uslibs/types.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/types.h
|
||||
1
janus/src/uslibs/xioctl.h
Symbolic link
1
janus/src/uslibs/xioctl.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/xioctl.h
|
||||
@@ -1,9 +1,9 @@
|
||||
FROM archlinux/archlinux:base-devel
|
||||
FROM archlinux/archlinux:base
|
||||
|
||||
RUN mkdir -p /etc/pacman.d/hooks \
|
||||
&& ln -s /dev/null /etc/pacman.d/hooks/30-systemd-tmpfiles.hook
|
||||
|
||||
RUN echo "Server = http://mirror.yandex.ru/archlinux/\$repo/os/\$arch" > /etc/pacman.d/mirrorlist
|
||||
RUN echo 'Server = https://mirror.rackspace.com/archlinux/$repo/os/$arch' > /etc/pacman.d/mirrorlist
|
||||
|
||||
RUN pacman -Syu --noconfirm \
|
||||
&& pacman -S --needed --noconfirm \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#define CHAR_BIT 8
|
||||
#define WITH_OMX
|
||||
#define WITH_GPIO
|
||||
#define JANUS_PLUGIN_INIT(...) { __VA_ARGS__ }
|
||||
#define EVTHREAD_USE_PTHREADS_IMPLEMENTED 1
|
||||
|
||||
@@ -15,15 +15,8 @@ disable =
|
||||
locally-disabled,
|
||||
fixme,
|
||||
missing-docstring,
|
||||
no-init,
|
||||
no-self-use,
|
||||
superfluous-parens,
|
||||
abstract-class-not-used,
|
||||
abstract-class-little-used,
|
||||
duplicate-code,
|
||||
bad-continuation,
|
||||
bad-whitespace,
|
||||
star-args,
|
||||
broad-except,
|
||||
redundant-keyword-arg,
|
||||
wrong-import-order,
|
||||
@@ -39,11 +32,8 @@ msg-template = {symbol} -- {path}:{line}({obj}): {msg}
|
||||
max-line-length = 160
|
||||
|
||||
[BASIC]
|
||||
# List of builtins function names that should not be used, separated by a comma
|
||||
bad-functions =
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma
|
||||
good-names = _, __, x, y, ws, make-html-h, make-jpeg-h
|
||||
good-names = _, __, 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,49 +3,49 @@ envlist = cppcheck, flake8, pylint, mypy, vulture, htmlhint
|
||||
skipsdist = true
|
||||
|
||||
[testenv]
|
||||
basepython = python3.9
|
||||
basepython = python3.11
|
||||
changedir = /src
|
||||
|
||||
[testenv:cppcheck]
|
||||
whitelist_externals = cppcheck
|
||||
allowlist_externals = cppcheck
|
||||
commands = cppcheck \
|
||||
--force \
|
||||
--std=c11 \
|
||||
--std=c17 \
|
||||
--error-exitcode=1 \
|
||||
--quiet \
|
||||
--enable=warning,unusedFunction,portability,performance,style \
|
||||
--enable=warning,portability,performance,style \
|
||||
--suppress=assignmentInAssert \
|
||||
--suppress=variableScope \
|
||||
--inline-suppr \
|
||||
--library=python \
|
||||
--include=linters/cppcheck.h \
|
||||
src python/ustreamer.c janus/rtp.h janus/rtp.c janus/plugin.c
|
||||
src python/src/*.? janus/src/*.?
|
||||
|
||||
[testenv:flake8]
|
||||
whitelist_externals = bash
|
||||
allowlist_externals = bash
|
||||
commands = bash -c 'flake8 --config=linters/flake8.ini tools/*.py' python/*.py
|
||||
deps =
|
||||
flake8
|
||||
flake8==5.0.4
|
||||
flake8-quotes
|
||||
|
||||
[testenv:pylint]
|
||||
whitelist_externals = bash
|
||||
allowlist_externals = bash
|
||||
commands = bash -c 'pylint --rcfile=linters/pylint.ini --output-format=colorized --reports=no tools/*.py python/*.py'
|
||||
deps =
|
||||
pylint
|
||||
|
||||
[testenv:mypy]
|
||||
whitelist_externals = bash
|
||||
allowlist_externals = bash
|
||||
commands = bash -c 'mypy --config-file=linters/mypy.ini tools/*.py python/*.py'
|
||||
deps =
|
||||
mypy
|
||||
|
||||
[testenv:vulture]
|
||||
whitelist_externals = bash
|
||||
allowlist_externals = bash
|
||||
commands = bash -c 'vulture tools/*.py python/*.py'
|
||||
deps =
|
||||
vulture
|
||||
|
||||
[testenv:htmlhint]
|
||||
whitelist_externals = htmlhint
|
||||
allowlist_externals = htmlhint
|
||||
commands = htmlhint src/ustreamer/http/data/*.html
|
||||
|
||||
@@ -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 4.10" "January 2021"
|
||||
.TH USTREAMER-DUMP 1 "version 6.9" "January 2021"
|
||||
|
||||
.SH NAME
|
||||
ustreamer-dump \- Dump uStreamer's memory sink to file
|
||||
@@ -44,6 +44,9 @@ Limit the number of frames. Default: 0 (infinite).
|
||||
.TP
|
||||
.BR \-i ", "\-\-interval\ \fIsec
|
||||
Delay between reading frames (float). Default: 0.
|
||||
.TP
|
||||
.BR \-k ", " \-\-key\-required
|
||||
Request keyframe from the sink. Default: disabled.
|
||||
|
||||
.SS "Logging options"
|
||||
.TP
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
.\" Manpage for ustreamer.
|
||||
.\" Open an issue or pull request to https://github.com/pikvm/ustreamer to correct errors or typos
|
||||
.TH USTREAMER 1 "version 4.10" "November 2020"
|
||||
.TH USTREAMER 1 "version 6.9" "November 2020"
|
||||
|
||||
.SH NAME
|
||||
ustreamer \- stream MJPG video from any V4L2 device to the network
|
||||
ustreamer \- stream MJPEG video from any V4L2 device to the network
|
||||
|
||||
.SH SYNOPSIS
|
||||
.B ustreamer
|
||||
.RI [OPTIONS]
|
||||
|
||||
.SH DESCRIPTION
|
||||
µStreamer (\fBustreamer\fP) is a lightweight and very quick server to stream MJPG video from any V4L2 device to the network. All new browsers have native support of this video format, as well as most video players such as mplayer, VLC etc. µStreamer is a part of the Pi-KVM project designed to stream VGA and HDMI screencast hardware data with the highest resolution and FPS possible.
|
||||
µStreamer (\fBustreamer\fP) is a lightweight and very quick server to stream MJPEG video from any V4L2 device to the network. All new browsers have native support of this video format, as well as most video players such as mplayer, VLC etc. µStreamer is a part of the PiKVM project designed to stream VGA and HDMI screencast hardware data with the highest resolution and FPS possible.
|
||||
|
||||
.SH USAGE
|
||||
Without arguments, \fBustreamer\fR will try to open \fB/dev/video0\fR with 640x480 resolution and start streaming on \fBhttp://127\.0\.0\.1:8080\fR\. You can override this behavior using parameters \fB\-\-device\fR, \fB\-\-host\fR and \fB\-\-port\fR\. For example, to stream to the world, run: \fBustreamer --device=/dev/video1 --host=0.0.0.0 --port=80\fR
|
||||
|
||||
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=omx \e\fR # Hardware encoding with OpenMAX
|
||||
\fB\-\-encoder=m2m-image \e\fR # Hardware encoding with V4L2 M2M intraface
|
||||
.nf
|
||||
\fB\-\-workers=3 \e\fR # Maximum workers for OpenMAX
|
||||
\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, RGB565, RGB24, JPEG; default: YUYV.
|
||||
.TP
|
||||
.BR \-a\ \fIstd ", " \-\-tv\-standard\ \fIstd
|
||||
Force TV standard.
|
||||
@@ -69,10 +69,10 @@ Desired FPS. Default: maximum possible.
|
||||
Drop frames smaller then this limit. Useful if the device produces small\-sized garbage frames. Default: 128 bytes.
|
||||
.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.
|
||||
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.
|
||||
@@ -84,27 +84,29 @@ Default: 1 (the number of CPU cores (but not more than 4)).
|
||||
.TP
|
||||
.BR \-q\ \fIN ", " \-\-quality\ \fIN
|
||||
Set quality of JPEG encoding from 1 to 100 (best). Default: 80.
|
||||
Note: If HW encoding is used (JPEG source format selected), this parameter attempts to configure the camera or capture device hardware's internal encoder. It does not re\-encode MJPG to MJPG to change the quality level for sources that already output MJPG.
|
||||
Note: If HW encoding is used (JPEG source format selected), this parameter attempts to configure the camera or capture device hardware's internal encoder. It does not re\-encode MJPEG to MJPEG to change the quality level for sources that already output MJPEG.
|
||||
.TP
|
||||
.BR \-c\ \fItype ", " \-\-encoder\ \fItype
|
||||
Use specified encoder. It may affect the number of workers.
|
||||
|
||||
CPU ─ Software MJPG encoding (default).
|
||||
CPU ─ Software MJPEG encoding (default).
|
||||
|
||||
OMX ─ GPU hardware accelerated MJPG encoding with OpenMax (required \fBWITH_OMX\fR feature).
|
||||
HW ─ Use pre-encoded MJPEG frames directly from camera hardware.
|
||||
|
||||
HW ─ Use pre-encoded MJPG frames directly from camera hardware.
|
||||
M2M-VIDEO ─ GPU-accelerated MJPEG encoding.
|
||||
|
||||
NOOP ─ Don't compress MJPG stream (do nothing).
|
||||
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. Required \fBWITH_OMX\fR feature.
|
||||
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.
|
||||
@@ -114,6 +116,9 @@ Timeout for device querying. Default: 1.
|
||||
.TP
|
||||
.BR \-\-device\-error\-delay\ \fIsec
|
||||
Delay before trying to connect to the device again after an error (timeout for example). Default: 1.
|
||||
.TP
|
||||
.BR \-\-m2m\-device\ \fI/dev/path
|
||||
Path to V4L2 mem-to-mem encoder device. Default: auto-select.
|
||||
|
||||
.SS "Image control options"
|
||||
.TP
|
||||
@@ -167,12 +172,15 @@ Bind to this TCP port. Default: 8080.
|
||||
.BR \-U\ \fIpath ", " \-\-unix\ \fIpath
|
||||
Bind to UNIX domain socket. Default: disabled.
|
||||
.TP
|
||||
.BR \-d ", " \-\-unix\-rm
|
||||
.BR \-D ", " \-\-unix\-rm
|
||||
Try to remove old unix socket file before binding. default: disabled.
|
||||
.TP
|
||||
.BR \-M\ \fImode ", " \-\-unix\-mode\ \fImode
|
||||
Set UNIX socket file permissions (like 777). Default: disabled.
|
||||
.TP
|
||||
.BR \-S ", " \-\-systemd
|
||||
Bind to systemd socket for socket activation. Required \fBWITH_SYSTEMD\fR feature. Default: disabled.
|
||||
.TP
|
||||
.BR \-\-user\ \fIname
|
||||
HTTP basic auth user. Default: disabled.
|
||||
.TP
|
||||
@@ -189,39 +197,40 @@ Don't send identical frames to clients, but no more than specified number. It ca
|
||||
Override image resolution for the /state. Default: disabled.
|
||||
.TP
|
||||
.BR \-\-tcp\-nodelay
|
||||
Set TCP_NODELAY flag to the client /stream socket. Ignored for \-\-unix.
|
||||
Set TCP_NODELAY flag to the client /stream socket. Only for TCP socket.
|
||||
Default: disabled.
|
||||
.TP
|
||||
.BR \-\-allow\-origin\ \fIstr
|
||||
Set Access\-Control\-Allow\-Origin header. Default: disabled.
|
||||
.TP
|
||||
.BR \-\-instance\-id\ \fIstr
|
||||
A short string identifier to be displayed in the /state handle. It must satisfy regexp ^[a-zA-Z0-9\./+_-]*$. Default: an empty string.
|
||||
.TP
|
||||
.BR \-\-server\-timeout\ \fIsec
|
||||
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
|
||||
Available only if \fBWITH_OMX\fR feature enabled.
|
||||
.TP
|
||||
.BR \-\-h264\-sink\ \fIname
|
||||
Use the specified shared memory object to sink H264 frames encoded by MMAL. 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.
|
||||
@@ -239,13 +248,36 @@ Timeout for lock. Default: 1.
|
||||
H264 bitrate in Kbps. Default: 5000.
|
||||
.TP
|
||||
.BR \-\-h264\-gop\ \fIN
|
||||
Intarval between keyframes. Default: 30.
|
||||
Interval between keyframes. Default: 30.
|
||||
.TP
|
||||
.BR \-\-h264\-m2m\-device\ \fI/dev/path
|
||||
Path to V4L2 mem-to-mem encoder device. Default: auto-select.
|
||||
|
||||
.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.
|
||||
.TP
|
||||
.BR \-\-exit\-on\-no\-clients \fIsec
|
||||
Exit the program if there have been no stream or sink clients or any HTTP requests in the last N seconds. Default: 0 (disabled).
|
||||
.TP
|
||||
.BR \-\-process\-name\-prefix\ \fIstr
|
||||
Set process name prefix which will be displayed in the process list like '\fIstr: ustreamer \-\-blah\-blah\-blah'\fR. Required \fBWITH_SETPROCTITLE\fR feature. Default: disabled.
|
||||
.TP
|
||||
|
||||
@@ -3,42 +3,31 @@
|
||||
|
||||
|
||||
pkgname=ustreamer
|
||||
pkgver=4.10
|
||||
pkgver=6.9
|
||||
pkgrel=1
|
||||
pkgdesc="Lightweight and fast MJPG-HTTP streamer"
|
||||
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)
|
||||
makedepends=(gcc make)
|
||||
depends=(libjpeg libevent libbsd libgpiod systemd)
|
||||
makedepends=(gcc make systemd)
|
||||
source=(${pkgname}::"git+https://github.com/pikvm/ustreamer#commit=v${pkgver}")
|
||||
md5sums=(SKIP)
|
||||
|
||||
|
||||
_options="WITH_GPIO=1"
|
||||
_options="WITH_GPIO=1 WITH_SYSTEMD=1"
|
||||
if [ -e /usr/bin/python3 ]; then
|
||||
_options="$_options WITH_PYTHON=1"
|
||||
depends+=(python)
|
||||
makedepends+=(python-setuptools)
|
||||
fi
|
||||
if [ -e /opt/vc/include/IL/OMX_Core.h ]; then
|
||||
depends+=(raspberrypi-firmware)
|
||||
makedepends+=(raspberrypi-firmware)
|
||||
_options="$_options WITH_OMX=1"
|
||||
makedepends+=(python-setuptools python-pip python-build python-wheel)
|
||||
fi
|
||||
if [ -e /usr/include/janus/plugins/plugin.h ];then
|
||||
depends+=(janus-gateway-pikvm)
|
||||
makedepends+=(janus-gateway-pikvm)
|
||||
depends+=(janus-gateway alsa-lib opus)
|
||||
makedepends+=(janus-gateway alsa-lib opus)
|
||||
_options="$_options WITH_JANUS=1"
|
||||
fi
|
||||
|
||||
|
||||
# LD does not link mmal with this option
|
||||
# This DOESN'T affect setup.py
|
||||
LDFLAGS="${LDFLAGS//--as-needed/}"
|
||||
export LDFLAGS="${LDFLAGS//,,/,}"
|
||||
|
||||
|
||||
build() {
|
||||
cd "$srcdir"
|
||||
rm -rf $pkgname-build
|
||||
|
||||
34
pkg/docker/Dockerfile.alpine
Normal file
34
pkg/docker/Dockerfile.alpine
Normal file
@@ -0,0 +1,34 @@
|
||||
FROM alpine:3.16 as build
|
||||
RUN apk add --no-cache \
|
||||
alpine-sdk \
|
||||
linux-headers \
|
||||
libjpeg-turbo-dev \
|
||||
libevent-dev \
|
||||
libbsd-dev \
|
||||
libgpiod-dev
|
||||
|
||||
WORKDIR /build/ustreamer/
|
||||
COPY . .
|
||||
RUN make -j5 WITH_GPIO=1
|
||||
|
||||
FROM alpine:3.16 as run
|
||||
|
||||
RUN apk add --no-cache \
|
||||
libevent \
|
||||
libjpeg-turbo \
|
||||
libevent \
|
||||
libgpiod \
|
||||
libbsd \
|
||||
v4l-utils
|
||||
|
||||
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
|
||||
COPY pkg/docker/entry.sh /
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/entry.sh"]
|
||||
CMD ["--dv-timings", "--format", "UYVY"]
|
||||
|
||||
# vim: syntax=dockerfile
|
||||
@@ -7,13 +7,12 @@ RUN apt-get update \
|
||||
gcc \
|
||||
libjpeg8-dev \
|
||||
libbsd-dev \
|
||||
libraspberrypi-dev \
|
||||
libgpiod-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /build/ustreamer/
|
||||
COPY . .
|
||||
RUN make -j5 WITH_OMX=1 WITH_GPIO=1
|
||||
RUN make -j5 WITH_GPIO=1
|
||||
RUN ["cross-build-end"]
|
||||
|
||||
FROM balenalib/raspberrypi3-debian:run as RUN
|
||||
|
||||
@@ -5,13 +5,12 @@ RUN apt-get update \
|
||||
gcc \
|
||||
libjpeg8-dev \
|
||||
libbsd-dev \
|
||||
libraspberrypi-dev \
|
||||
libgpiod-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /build/ustreamer/
|
||||
COPY . .
|
||||
RUN make -j5 WITH_OMX=1 WITH_GPIO=1
|
||||
RUN make -j5 WITH_GPIO=1
|
||||
|
||||
FROM balenalib/raspberrypi3-debian:run as RUN
|
||||
|
||||
|
||||
14
pkg/docker/entry.sh
Executable file
14
pkg/docker/entry.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
[ -n "$EDID" ] && {
|
||||
[ -n "$EDID_HEX" ] && echo "$EDID_HEX" > /edid.hex
|
||||
while true; do
|
||||
v4l2-ctl --device=/dev/video0 --set-edid=file=/edid.hex --fix-edid-checksums --info-edid && break
|
||||
echo 'Failed to set EDID. Reetrying...'
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
./ustreamer --host=0.0.0.0 $@
|
||||
@@ -5,7 +5,7 @@ EAPI=7
|
||||
|
||||
inherit git-r3
|
||||
|
||||
DESCRIPTION="uStreamer - Lightweight and fast MJPG-HTTP streamer"
|
||||
DESCRIPTION="uStreamer - Lightweight and fast MJPEG-HTTP streamer"
|
||||
HOMEPAGE="https://github.com/pikvm/ustreamer"
|
||||
EGIT_REPO_URI="https://github.com/pikvm/ustreamer.git"
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=ustreamer
|
||||
PKG_VERSION:=4.10
|
||||
PKG_VERSION:=6.9
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=Maxim Devaev <mdevaev@gmail.com>
|
||||
|
||||
@@ -30,7 +30,7 @@ define Package/ustreamer
|
||||
endef
|
||||
|
||||
define Package/ustreamer/description
|
||||
µStreamer - Lightweight and fast MJPG-HTTP streamer
|
||||
µStreamer - Lightweight and fast MJPEG-HTTP streamer
|
||||
endef
|
||||
|
||||
define Package/ustreamer/install
|
||||
|
||||
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,6 +1,6 @@
|
||||
-include ../config.mk
|
||||
|
||||
DESTDIR ?=
|
||||
R_DESTDIR ?=
|
||||
PREFIX ?= /usr/local
|
||||
|
||||
PY ?= python3
|
||||
@@ -9,12 +9,14 @@ PY ?= python3
|
||||
# =====
|
||||
all:
|
||||
$(info == PY_BUILD ustreamer-*.so)
|
||||
@ $(PY) setup.py build
|
||||
rm -rf root
|
||||
$(ECHO) $(PY) -m build --skip-dependency-check --no-isolation
|
||||
$(ECHO) $(PY) -m pip install dist/*.whl --ignore-installed --root=./root
|
||||
|
||||
|
||||
install:
|
||||
$(PY) setup.py install --prefix=$(PREFIX) --root=$(if $(DESTDIR),$(DESTDIR),/)
|
||||
$(PY) -m pip install dist/*.whl --ignore-installed --prefix=$(PREFIX) --root=$(if $(R_DESTDIR),$(R_DESTDIR),/)
|
||||
|
||||
|
||||
clean:
|
||||
rm -rf build
|
||||
rm -rf root dist ustreamer.egg-info
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import os
|
||||
|
||||
from distutils.core import Extension
|
||||
from distutils.core import setup
|
||||
from setuptools import Extension
|
||||
from setuptools import setup
|
||||
|
||||
|
||||
# =====
|
||||
def _find_sources(suffix: str) -> list[str]:
|
||||
sources: list[str] = []
|
||||
for (root_path, _, names) in os.walk("src"):
|
||||
for name in names:
|
||||
if name.endswith(suffix):
|
||||
sources.append(os.path.join(root_path, name))
|
||||
return sources
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
setup(
|
||||
name="ustreamer",
|
||||
version="4.10",
|
||||
version="6.9",
|
||||
description="uStreamer tools",
|
||||
author="Maxim Devaev",
|
||||
author_email="mdevaev@gmail.com",
|
||||
@@ -17,9 +26,9 @@ if __name__ == "__main__":
|
||||
Extension(
|
||||
"ustreamer",
|
||||
libraries=["rt", "m", "pthread"],
|
||||
extra_compile_args=["-std=c17", "-D_GNU_SOURCE"],
|
||||
undef_macros=["NDEBUG"],
|
||||
sources=["src/" + name for name in os.listdir("src") if name.endswith(".c")],
|
||||
depends=["src/" + name for name in os.listdir("src") if name.endswith(".h")],
|
||||
sources=_find_sources(".c"),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../src/libs/frame.c
|
||||
@@ -1 +0,0 @@
|
||||
../../src/libs/frame.h
|
||||
@@ -1 +0,0 @@
|
||||
../../src/libs/memsinksh.h
|
||||
@@ -1 +0,0 @@
|
||||
../../src/libs/tools.h
|
||||
1
python/src/uslibs/frame.c
Symbolic link
1
python/src/uslibs/frame.c
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/frame.c
|
||||
1
python/src/uslibs/frame.h
Symbolic link
1
python/src/uslibs/frame.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/frame.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/memsinksh.h
Symbolic link
1
python/src/uslibs/memsinksh.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/memsinksh.h
|
||||
1
python/src/uslibs/tools.h
Symbolic link
1
python/src/uslibs/tools.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/tools.h
|
||||
1
python/src/uslibs/types.h
Symbolic link
1
python/src/uslibs/types.h
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../src/libs/types.h
|
||||
@@ -13,9 +13,10 @@
|
||||
|
||||
#include <Python.h>
|
||||
|
||||
#include "tools.h"
|
||||
#include "frame.h"
|
||||
#include "memsinksh.h"
|
||||
#include "uslibs/types.h"
|
||||
#include "uslibs/tools.h"
|
||||
#include "uslibs/frame.h"
|
||||
#include "uslibs/memsinksh.h"
|
||||
|
||||
|
||||
typedef struct {
|
||||
@@ -25,36 +26,27 @@ typedef struct {
|
||||
double lock_timeout;
|
||||
double wait_timeout;
|
||||
double drop_same_frames;
|
||||
uz data_size;
|
||||
|
||||
int fd;
|
||||
memsink_shared_s *mem;
|
||||
us_memsink_shared_s *mem;
|
||||
|
||||
uint64_t frame_id;
|
||||
long double frame_ts;
|
||||
frame_s *frame;
|
||||
} MemsinkObject;
|
||||
u64 frame_id;
|
||||
ldf frame_ts;
|
||||
us_frame_s *frame;
|
||||
} _MemsinkObject;
|
||||
|
||||
|
||||
#define MEM(_next) self->mem->_next
|
||||
#define FRAME(_next) self->frame->_next
|
||||
|
||||
|
||||
static void MemsinkObject_destroy_internals(MemsinkObject *self) {
|
||||
static void _MemsinkObject_destroy_internals(_MemsinkObject *self) {
|
||||
if (self->mem != NULL) {
|
||||
memsink_shared_unmap(self->mem);
|
||||
us_memsink_shared_unmap(self->mem, self->data_size);
|
||||
self->mem = NULL;
|
||||
}
|
||||
if (self->fd > 0) {
|
||||
close(self->fd);
|
||||
self->fd = -1;
|
||||
}
|
||||
if (self->frame) {
|
||||
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) {
|
||||
static int _MemsinkObject_init(_MemsinkObject *self, PyObject *args, PyObject *kwargs) {
|
||||
self->lock_timeout = 1;
|
||||
self->wait_timeout = 1;
|
||||
|
||||
@@ -65,139 +57,155 @@ static int MemsinkObject_init(MemsinkObject *self, PyObject *args, PyObject *kwa
|
||||
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
|
||||
|
||||
self->frame = frame_init();
|
||||
if ((self->data_size = us_memsink_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 = memsink_shared_map(self->fd)) == NULL) {
|
||||
if ((self->mem = us_memsink_shared_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) {
|
||||
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);
|
||||
}
|
||||
|
||||
static void MemsinkObject_dealloc(MemsinkObject *self) {
|
||||
MemsinkObject_destroy_internals(self);
|
||||
static void _MemsinkObject_dealloc(_MemsinkObject *self) {
|
||||
_MemsinkObject_destroy_internals(self);
|
||||
PyObject_Del(self);
|
||||
}
|
||||
|
||||
static PyObject *MemsinkObject_close(MemsinkObject *self, PyObject *Py_UNUSED(ignored)) {
|
||||
MemsinkObject_destroy_internals(self);
|
||||
static PyObject *_MemsinkObject_close(_MemsinkObject *self, PyObject *Py_UNUSED(ignored)) {
|
||||
_MemsinkObject_destroy_internals(self);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject *MemsinkObject_enter(MemsinkObject *self, PyObject *Py_UNUSED(ignored)) {
|
||||
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", "");
|
||||
static PyObject *_MemsinkObject_exit(_MemsinkObject *self, PyObject *Py_UNUSED(ignored)) {
|
||||
return PyObject_CallMethod((PyObject*)self, "close", "");
|
||||
}
|
||||
|
||||
static int wait_frame(MemsinkObject *self) {
|
||||
long double deadline_ts = get_now_monotonic() + self->wait_timeout;
|
||||
static int _wait_frame(_MemsinkObject *self) {
|
||||
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
|
||||
|
||||
int retval = flock_timedwait_monotonic(self->fd, self->lock_timeout);
|
||||
now = get_now_monotonic();
|
||||
|
||||
if (retval < 0 && errno != EWOULDBLOCK) {
|
||||
RETURN_OS_ERROR;
|
||||
|
||||
} else if (retval == 0) {
|
||||
if (MEM(magic) == MEMSINK_MAGIC && MEM(version) == MEMSINK_VERSION && MEM(id) != self->frame_id) {
|
||||
if (self->drop_same_frames > 0) {
|
||||
if (
|
||||
FRAME_COMPARE_META_USED_NOTS(self->mem, self->frame)
|
||||
&& (self->frame_ts + self->drop_same_frames > now)
|
||||
&& !memcmp(FRAME(data), MEM(data), MEM(used))
|
||||
) {
|
||||
self->frame_id = MEM(id);
|
||||
goto drop;
|
||||
}
|
||||
}
|
||||
|
||||
Py_BLOCK_THREADS
|
||||
return 0;
|
||||
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;
|
||||
|
||||
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_memsink_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);
|
||||
|
||||
# undef RETURN_OS_ERROR
|
||||
|
||||
} while (now_ts < deadline_ts);
|
||||
return -2;
|
||||
}
|
||||
|
||||
static PyObject *MemsinkObject_wait_frame(MemsinkObject *self, PyObject *Py_UNUSED(ignored)) {
|
||||
static PyObject *_MemsinkObject_wait_frame(_MemsinkObject *self, PyObject *args, PyObject *kwargs) {
|
||||
if (self->mem == NULL || self->fd <= 0) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Closed");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
switch (wait_frame(self)) {
|
||||
bool key_required = false;
|
||||
static char *kws[] = {"key_required", NULL};
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|p", kws, &key_required)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
switch (_wait_frame(self)) {
|
||||
case 0: break;
|
||||
case -2: Py_RETURN_NONE;
|
||||
default: return NULL;
|
||||
}
|
||||
|
||||
frame_set_data(self->frame, MEM(data), MEM(used));
|
||||
FRAME_COPY_META(self->mem, self->frame);
|
||||
self->frame_id = MEM(id);
|
||||
self->frame_ts = get_now_monotonic();
|
||||
MEM(last_client_ts) = self->frame_ts;
|
||||
us_memsink_shared_s *mem = self->mem;
|
||||
us_frame_set_data(self->frame, us_memsink_get_data(mem), mem->used);
|
||||
US_FRAME_COPY_META(self->mem, self->frame);
|
||||
self->frame_id = mem->id;
|
||||
self->frame_ts = us_get_now_monotonic();
|
||||
if (key_required) {
|
||||
mem->key_requested = true;
|
||||
}
|
||||
|
||||
if (flock(self->fd, LOCK_UN) < 0) {
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
@@ -208,18 +216,18 @@ static PyObject *MemsinkObject_wait_frame(MemsinkObject *self, PyObject *Py_UNUS
|
||||
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);
|
||||
@@ -227,10 +235,11 @@ static PyObject *MemsinkObject_wait_frame(MemsinkObject *self, PyObject *Py_UNUS
|
||||
SET_NUMBER(stride, Long, Long);
|
||||
SET_NUMBER(online, Long, Bool);
|
||||
SET_NUMBER(key, Long, Bool);
|
||||
SET_NUMBER(gop, Long, Long);
|
||||
SET_NUMBER(grab_ts, Double, Float);
|
||||
SET_NUMBER(encode_begin_ts, Double, Float);
|
||||
SET_NUMBER(encode_end_ts, Double, Float);
|
||||
SET_VALUE("data", PyBytes_FromStringAndSize((const char *)FRAME(data), FRAME(used)));
|
||||
SET_VALUE("data", PyBytes_FromStringAndSize((const char*)self->frame->data, self->frame->used));
|
||||
|
||||
# undef SET_NUMBER
|
||||
# undef SET_VALUE
|
||||
@@ -238,36 +247,34 @@ static PyObject *MemsinkObject_wait_frame(MemsinkObject *self, PyObject *Py_UNUS
|
||||
return dict_frame;
|
||||
}
|
||||
|
||||
static PyObject *MemsinkObject_is_opened(MemsinkObject *self, PyObject *Py_UNUSED(ignored)) {
|
||||
static PyObject *_MemsinkObject_is_opened(_MemsinkObject *self, PyObject *Py_UNUSED(ignored)) {
|
||||
return PyBool_FromLong(self->mem != NULL && self->fd > 0);
|
||||
}
|
||||
|
||||
#define FIELD_GETTER(_field, _from, _to) \
|
||||
static PyObject *MemsinkObject_getter_##_field(MemsinkObject *self, void *Py_UNUSED(closure)) { \
|
||||
return Py##_to##_From##_from(self->_field); \
|
||||
#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)}
|
||||
static PyMethodDef _MemsinkObject_methods[] = {
|
||||
# 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),
|
||||
ADD_METHOD("wait_frame", wait_frame, METH_NOARGS),
|
||||
ADD_METHOD("wait_frame", wait_frame, METH_VARARGS | METH_KEYWORDS),
|
||||
ADD_METHOD("is_opened", is_opened, METH_NOARGS),
|
||||
{},
|
||||
# undef ADD_METHOD
|
||||
};
|
||||
|
||||
static PyGetSetDef MemsinkObject_getsets[] = {
|
||||
# define ADD_GETTER(_field) {.name = #_field, .get = (getter)MemsinkObject_getter_##_field}
|
||||
static PyGetSetDef _MemsinkObject_getsets[] = {
|
||||
# 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),
|
||||
@@ -276,43 +283,40 @@ static PyGetSetDef MemsinkObject_getsets[] = {
|
||||
# undef ADD_GETTER
|
||||
};
|
||||
|
||||
static PyTypeObject MemsinkType = {
|
||||
static PyTypeObject _MemsinkType = {
|
||||
PyVarObject_HEAD_INIT(NULL, 0)
|
||||
.tp_name = "ustreamer.Memsink",
|
||||
.tp_basicsize = sizeof(MemsinkObject),
|
||||
.tp_basicsize = sizeof(_MemsinkObject),
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_new = PyType_GenericNew,
|
||||
.tp_init = (initproc)MemsinkObject_init,
|
||||
.tp_dealloc = (destructor)MemsinkObject_dealloc,
|
||||
.tp_repr = (reprfunc)MemsinkObject_repr,
|
||||
.tp_methods = MemsinkObject_methods,
|
||||
.tp_getset = MemsinkObject_getsets,
|
||||
.tp_init = (initproc)_MemsinkObject_init,
|
||||
.tp_dealloc = (destructor)_MemsinkObject_dealloc,
|
||||
.tp_repr = (reprfunc)_MemsinkObject_repr,
|
||||
.tp_methods = _MemsinkObject_methods,
|
||||
.tp_getset = _MemsinkObject_getsets,
|
||||
};
|
||||
|
||||
static PyModuleDef ustreamer_Module = {
|
||||
static PyModuleDef _Module = {
|
||||
PyModuleDef_HEAD_INIT,
|
||||
.m_name = "ustreamer",
|
||||
.m_size = -1,
|
||||
};
|
||||
|
||||
PyMODINIT_FUNC PyInit_ustreamer(void) { // cppcheck-suppress unusedFunction
|
||||
PyObject *module = PyModule_Create(&ustreamer_Module);
|
||||
PyMODINIT_FUNC PyInit_ustreamer(void) {
|
||||
PyObject *module = PyModule_Create(&_Module);
|
||||
if (module == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (PyType_Ready(&MemsinkType) < 0) {
|
||||
if (PyType_Ready(&_MemsinkType) < 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Py_INCREF(&MemsinkType);
|
||||
Py_INCREF(&_MemsinkType);
|
||||
|
||||
if (PyModule_AddObject(module, "Memsink", (PyObject *)&MemsinkType) < 0) {
|
||||
if (PyModule_AddObject(module, "Memsink", (PyObject*)&_MemsinkType) < 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
#undef FRAME
|
||||
#undef MEM
|
||||
|
||||
92
src/Makefile
92
src/Makefile
@@ -1,24 +1,22 @@
|
||||
DESTDIR ?=
|
||||
R_DESTDIR ?=
|
||||
PREFIX ?= /usr/local
|
||||
|
||||
CC ?= gcc
|
||||
CFLAGS ?= -O3
|
||||
LDFLAGS ?=
|
||||
|
||||
RPI_VC_HEADERS ?= /opt/vc/include
|
||||
RPI_VC_LIBS ?= /opt/vc/lib
|
||||
|
||||
|
||||
# =====
|
||||
_USTR = ustreamer.bin
|
||||
_DUMP = ustreamer-dump.bin
|
||||
_V4P = ustreamer-v4p.bin
|
||||
|
||||
_CFLAGS = -MD -c -std=c11 -Wall -Wextra -D_GNU_SOURCE $(CFLAGS)
|
||||
_LDFLAGS = $(LDFLAGS)
|
||||
_CFLAGS = -MD -c -std=c17 -Wall -Wextra -D_GNU_SOURCE $(CFLAGS)
|
||||
|
||||
_COMMON_LIBS = -lm -ljpeg -pthread -lrt
|
||||
_USTR_LDFLAGS = $(LDFLAGS) -lm -ljpeg -pthread -lrt -latomic -levent -levent_pthreads
|
||||
_DUMP_LDFLAGS = $(LDFLAGS) -lm -ljpeg -pthread -lrt -latomic
|
||||
_V4P_LDFLAGS = $(LDFLAGS) -lm -ljpeg -pthread -lrt -latomic
|
||||
|
||||
_USTR_LIBS = $(_COMMON_LIBS) -levent -levent_pthreads
|
||||
_USTR_SRCS = $(shell ls \
|
||||
libs/*.c \
|
||||
ustreamer/*.c \
|
||||
@@ -26,36 +24,43 @@ _USTR_SRCS = $(shell ls \
|
||||
ustreamer/data/*.c \
|
||||
ustreamer/encoders/cpu/*.c \
|
||||
ustreamer/encoders/hw/*.c \
|
||||
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
|
||||
|
||||
|
||||
_TARGETS = $(_USTR) $(_DUMP)
|
||||
_OBJS = $(_USTR_SRCS:%.c=$(_BUILD)/%.o) $(_DUMP_SRCS:%.c=$(_BUILD)/%.o)
|
||||
|
||||
|
||||
define optbool
|
||||
$(filter $(shell echo $(1) | tr A-Z a-z), yes on 1)
|
||||
endef
|
||||
|
||||
|
||||
ifneq ($(call optbool,$(WITH_OMX)),)
|
||||
_USTR_LIBS += -lbcm_host -lvcos -lvcsm -lopenmaxil -lmmal -lmmal_core -lmmal_util -lmmal_vc_client -lmmal_components -L$(RPI_VC_LIBS)
|
||||
override _CFLAGS += -DWITH_OMX -DOMX_SKIP64BIT -I$(RPI_VC_HEADERS)
|
||||
_USTR_SRCS += $(shell ls \
|
||||
ustreamer/encoders/omx/*.c \
|
||||
ustreamer/h264/*.c \
|
||||
)
|
||||
ifneq ($(call optbool,$(WITH_GPIO)),)
|
||||
override _CFLAGS += -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
|
||||
|
||||
|
||||
ifneq ($(call optbool,$(WITH_GPIO)),)
|
||||
_USTR_LIBS += -lgpiod
|
||||
override _CFLAGS += -DWITH_GPIO
|
||||
_USTR_SRCS += $(shell ls ustreamer/gpio/*.c)
|
||||
ifneq ($(call optbool,$(WITH_SYSTEMD)),)
|
||||
override _CFLAGS += -DWITH_SYSTEMD
|
||||
override _USTR_LDFLAGS += -lsystemd
|
||||
override _USTR_SRCS += $(shell ls ustreamer/http/systemd/*.c)
|
||||
endif
|
||||
|
||||
|
||||
@@ -67,47 +72,64 @@ endif
|
||||
|
||||
WITH_SETPROCTITLE ?= 1
|
||||
ifneq ($(call optbool,$(WITH_SETPROCTITLE)),)
|
||||
ifeq ($(shell uname -s | tr A-Z a-z),linux)
|
||||
_USTR_LIBS += -lbsd
|
||||
endif
|
||||
override _CFLAGS += -DWITH_SETPROCTITLE
|
||||
ifeq ($(shell uname -s | tr A-Z a-z),linux)
|
||||
override _USTR_LDFLAGS += -lbsd
|
||||
endif
|
||||
endif
|
||||
|
||||
|
||||
WITH_V4P ?= 0
|
||||
ifneq ($(call optbool,$(WITH_V4P)),)
|
||||
override _TARGETS += $(_V4P)
|
||||
override _OBJS += $(_V4P_SRCS:%.c=$(_BUILD)/%.o)
|
||||
override _CFLAGS += -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)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPG-HTTP streamer. #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2021 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,17 +23,17 @@
|
||||
#include "file.h"
|
||||
|
||||
|
||||
output_file_s *output_file_init(const char *path, bool json) {
|
||||
output_file_s *output;
|
||||
A_CALLOC(output, 1);
|
||||
us_output_file_s *us_output_file_init(const char *path, bool json) {
|
||||
us_output_file_s *output;
|
||||
US_CALLOC(output, 1);
|
||||
|
||||
if (!strcmp(path, "-")) {
|
||||
LOG_INFO("Using output: <stdout>");
|
||||
US_LOG_INFO("Using output: <stdout>");
|
||||
output->fp = stdout;
|
||||
} else {
|
||||
LOG_INFO("Using output: %s", path);
|
||||
US_LOG_INFO("Using output: %s", path);
|
||||
if ((output->fp = fopen(path, "wb")) == NULL) {
|
||||
LOG_PERROR("Can't open output file");
|
||||
US_LOG_PERROR("Can't open output file");
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
@@ -42,21 +42,21 @@ output_file_s *output_file_init(const char *path, bool json) {
|
||||
return output;
|
||||
|
||||
error:
|
||||
output_file_destroy(output);
|
||||
us_output_file_destroy(output);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void output_file_write(void *v_output, const frame_s *frame) {
|
||||
output_file_s *output = (output_file_s *)v_output;
|
||||
void us_output_file_write(void *v_output, const us_frame_s *frame) {
|
||||
us_output_file_s *output = v_output;
|
||||
if (output->json) {
|
||||
base64_encode(frame->data, frame->used, &output->base64_data, &output->base64_allocated);
|
||||
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,"
|
||||
" \"format\": %u, \"stride\": %u, \"online\": %u, \"key\": %u, \"gop\": %u,"
|
||||
" \"grab_ts\": %.3Lf, \"encode_begin_ts\": %.3Lf, \"encode_end_ts\": %.3Lf,"
|
||||
" \"data\": \"%s\"}\n",
|
||||
frame->used, frame->width, frame->height,
|
||||
frame->format, frame->stride, frame->online,
|
||||
frame->format, frame->stride, frame->online, frame->key, frame->gop,
|
||||
frame->grab_ts, frame->encode_begin_ts, frame->encode_end_ts,
|
||||
output->base64_data);
|
||||
} else {
|
||||
@@ -65,14 +65,12 @@ void output_file_write(void *v_output, const frame_s *frame) {
|
||||
fflush(output->fp);
|
||||
}
|
||||
|
||||
void output_file_destroy(void *v_output) {
|
||||
output_file_s *output = (output_file_s *)v_output;
|
||||
if (output->base64_data) {
|
||||
free(output->base64_data);
|
||||
}
|
||||
void us_output_file_destroy(void *v_output) {
|
||||
us_output_file_s *output = v_output;
|
||||
US_DELETE(output->base64_data, free);
|
||||
if (output->fp && output->fp != stdout) {
|
||||
if (fclose(output->fp) < 0) {
|
||||
LOG_PERROR("Can't close output file");
|
||||
US_LOG_PERROR("Can't close output file");
|
||||
}
|
||||
}
|
||||
free(output);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPG-HTTP streamer. #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2021 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 #
|
||||
@@ -41,9 +41,9 @@ typedef struct {
|
||||
FILE *fp;
|
||||
char *base64_data;
|
||||
size_t base64_allocated;
|
||||
} output_file_s;
|
||||
} us_output_file_s;
|
||||
|
||||
|
||||
output_file_s *output_file_init(const char *path, bool json);
|
||||
void output_file_write(void *v_output, const frame_s *frame);
|
||||
void output_file_destroy(void *v_output);
|
||||
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);
|
||||
|
||||
149
src/dump/main.c
149
src/dump/main.c
@@ -1,8 +1,8 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPG-HTTP streamer. #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2021 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,18 @@
|
||||
#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/config.h"
|
||||
#include "../libs/const.h"
|
||||
#include "../libs/tools.h"
|
||||
#include "../libs/logging.h"
|
||||
#include "../libs/frame.h"
|
||||
#include "../libs/memsink.h"
|
||||
#include "../libs/signal.h"
|
||||
#include "../libs/options.h"
|
||||
|
||||
#include "file.h"
|
||||
@@ -48,6 +48,7 @@ enum _OPT_VALUES {
|
||||
_O_OUTPUT_JSON = 'j',
|
||||
_O_COUNT = 'c',
|
||||
_O_INTERVAL = 'i',
|
||||
_O_KEY_REQUIRED = 'k',
|
||||
|
||||
_O_HELP = 'h',
|
||||
_O_VERSION = 'v',
|
||||
@@ -67,6 +68,7 @@ static const struct option _LONG_OPTS[] = {
|
||||
{"output-json", no_argument, NULL, _O_OUTPUT_JSON},
|
||||
{"count", required_argument, NULL, _O_COUNT},
|
||||
{"interval", required_argument, NULL, _O_INTERVAL},
|
||||
{"key-required", no_argument, NULL, _O_KEY_REQUIRED},
|
||||
|
||||
{"log-level", required_argument, NULL, _O_LOG_LEVEL},
|
||||
{"perf", no_argument, NULL, _O_PERF},
|
||||
@@ -82,30 +84,30 @@ static const struct option _LONG_OPTS[] = {
|
||||
};
|
||||
|
||||
|
||||
volatile bool global_stop = false;
|
||||
volatile bool _g_stop = false;
|
||||
|
||||
|
||||
typedef struct {
|
||||
void *v_output;
|
||||
void (*write)(void *v_output, const frame_s *frame);
|
||||
void (*write)(void *v_output, const us_frame_s *frame);
|
||||
void (*destroy)(void *v_output);
|
||||
} _output_context_s;
|
||||
|
||||
|
||||
static void _signal_handler(int signum);
|
||||
static void _install_signal_handlers(void);
|
||||
|
||||
static int _dump_sink(
|
||||
const char *sink_name, unsigned sink_timeout,
|
||||
long long count, long double interval,
|
||||
bool key_required,
|
||||
_output_context_s *ctx);
|
||||
|
||||
static void _help(FILE *fp);
|
||||
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
LOGGING_INIT;
|
||||
A_THREAD_RENAME("main");
|
||||
US_LOGGING_INIT;
|
||||
US_THREAD_RENAME("main");
|
||||
|
||||
char *sink_name = NULL;
|
||||
unsigned sink_timeout = 1;
|
||||
@@ -113,6 +115,7 @@ int main(int argc, char *argv[]) {
|
||||
bool output_json = false;
|
||||
long long count = 0;
|
||||
long double interval = 0;
|
||||
bool key_required = false;
|
||||
|
||||
# define OPT_SET(_dest, _value) { \
|
||||
_dest = _value; \
|
||||
@@ -140,7 +143,7 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
|
||||
char short_opts[128];
|
||||
build_short_options(_LONG_OPTS, short_opts, 128);
|
||||
us_build_short_options(_LONG_OPTS, short_opts, 128);
|
||||
|
||||
for (int ch; (ch = getopt_long(argc, argv, short_opts, _LONG_OPTS, NULL)) >= 0;) {
|
||||
switch (ch) {
|
||||
@@ -150,16 +153,17 @@ int main(int argc, char *argv[]) {
|
||||
case _O_OUTPUT_JSON: OPT_SET(output_json, true);
|
||||
case _O_COUNT: OPT_NUMBER("--count", count, 0, LLONG_MAX, 0);
|
||||
case _O_INTERVAL: OPT_LDOUBLE("--interval", interval, 0, 60);
|
||||
case _O_KEY_REQUIRED: OPT_SET(key_required, true);
|
||||
|
||||
case _O_LOG_LEVEL: OPT_NUMBER("--log-level", us_log_level, LOG_LEVEL_INFO, LOG_LEVEL_DEBUG, 0);
|
||||
case _O_PERF: OPT_SET(us_log_level, LOG_LEVEL_PERF);
|
||||
case _O_VERBOSE: OPT_SET(us_log_level, LOG_LEVEL_VERBOSE);
|
||||
case _O_DEBUG: OPT_SET(us_log_level, LOG_LEVEL_DEBUG);
|
||||
case _O_FORCE_LOG_COLORS: OPT_SET(us_log_colored, true);
|
||||
case _O_NO_LOG_COLORS: OPT_SET(us_log_colored, false);
|
||||
case _O_LOG_LEVEL: OPT_NUMBER("--log-level", us_g_log_level, US_LOG_LEVEL_INFO, US_LOG_LEVEL_DEBUG, 0);
|
||||
case _O_PERF: OPT_SET(us_g_log_level, US_LOG_LEVEL_PERF);
|
||||
case _O_VERBOSE: OPT_SET(us_g_log_level, US_LOG_LEVEL_VERBOSE);
|
||||
case _O_DEBUG: OPT_SET(us_g_log_level, US_LOG_LEVEL_DEBUG);
|
||||
case _O_FORCE_LOG_COLORS: OPT_SET(us_g_log_colored, true);
|
||||
case _O_NO_LOG_COLORS: OPT_SET(us_g_log_colored, false);
|
||||
|
||||
case _O_HELP: _help(stdout); return 0;
|
||||
case _O_VERSION: puts(VERSION); return 0;
|
||||
case _O_VERSION: puts(US_VERSION); return 0;
|
||||
|
||||
case 0: break;
|
||||
default: return 1;
|
||||
@@ -175,19 +179,18 @@ int main(int argc, char *argv[]) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
_output_context_s ctx;
|
||||
MEMSET_ZERO(ctx);
|
||||
_output_context_s ctx = {0};
|
||||
|
||||
if (output_path && output_path[0] != '\0') {
|
||||
if ((ctx.v_output = (void *)output_file_init(output_path, output_json)) == NULL) {
|
||||
if ((ctx.v_output = (void*)us_output_file_init(output_path, output_json)) == NULL) {
|
||||
return 1;
|
||||
}
|
||||
ctx.write = output_file_write;
|
||||
ctx.destroy = output_file_destroy;
|
||||
ctx.write = us_output_file_write;
|
||||
ctx.destroy = us_output_file_destroy;
|
||||
}
|
||||
|
||||
_install_signal_handlers();
|
||||
int retval = abs(_dump_sink(sink_name, sink_timeout, count, interval, &ctx));
|
||||
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);
|
||||
}
|
||||
@@ -196,50 +199,30 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
|
||||
static void _signal_handler(int signum) {
|
||||
switch (signum) {
|
||||
case SIGTERM: LOG_INFO_NOLOCK("===== Stopping by SIGTERM ====="); break;
|
||||
case SIGINT: LOG_INFO_NOLOCK("===== Stopping by SIGINT ====="); break;
|
||||
case SIGPIPE: LOG_INFO_NOLOCK("===== Stopping by SIGPIPE ====="); break;
|
||||
default: LOG_INFO_NOLOCK("===== Stopping by %d =====", signum); break;
|
||||
}
|
||||
global_stop = true;
|
||||
}
|
||||
|
||||
static void _install_signal_handlers(void) {
|
||||
struct sigaction sig_act;
|
||||
MEMSET_ZERO(sig_act);
|
||||
|
||||
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));
|
||||
|
||||
LOG_DEBUG("Installing SIGINT handler ...");
|
||||
assert(!sigaction(SIGINT, &sig_act, NULL));
|
||||
|
||||
LOG_DEBUG("Installing SIGTERM handler ...");
|
||||
assert(!sigaction(SIGTERM, &sig_act, NULL));
|
||||
|
||||
LOG_DEBUG("Installing SIGTERM handler ...");
|
||||
assert(!sigaction(SIGPIPE, &sig_act, NULL));
|
||||
char *const name = us_signum_to_string(signum);
|
||||
US_LOG_INFO_NOLOCK("===== Stopping by %s =====", name);
|
||||
free(name);
|
||||
_g_stop = true;
|
||||
}
|
||||
|
||||
static int _dump_sink(
|
||||
const char *sink_name, unsigned sink_timeout,
|
||||
long long count, long double interval,
|
||||
bool key_required,
|
||||
_output_context_s *ctx) {
|
||||
|
||||
int retval = -1;
|
||||
|
||||
if (count == 0) {
|
||||
count = -1;
|
||||
}
|
||||
|
||||
useconds_t interval_us = interval * 1000000;
|
||||
const useconds_t interval_us = interval * 1000000;
|
||||
|
||||
frame_s *frame = frame_init();
|
||||
memsink_s *sink = NULL;
|
||||
us_frame_s *frame = us_frame_init();
|
||||
us_memsink_s *sink = NULL;
|
||||
|
||||
if ((sink = memsink_init("input", sink_name, false, 0, false, 0, sink_timeout)) == NULL) {
|
||||
if ((sink = us_memsink_init("input", sink_name, false, 0, false, 0, sink_timeout)) == NULL) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
@@ -249,32 +232,36 @@ static int _dump_sink(
|
||||
|
||||
long double last_ts = 0;
|
||||
|
||||
while (!global_stop) {
|
||||
int error = memsink_client_get(sink, frame);
|
||||
while (!_g_stop) {
|
||||
bool key_requested;
|
||||
const int error = us_memsink_client_get(sink, frame, &key_requested, key_required);
|
||||
if (error == 0) {
|
||||
const long double now = get_now_monotonic();
|
||||
const long long now_second = floor_ms(now);
|
||||
key_required = false;
|
||||
|
||||
const long double now = us_get_now_monotonic();
|
||||
const long long now_second = us_floor_ms(now);
|
||||
|
||||
char fourcc_str[8];
|
||||
LOG_VERBOSE("Frame: size=%zu, res=%ux%u, fourcc=%s, stride=%u, online=%d, key=%d, latency=%.3Lf, diff=%.3Lf",
|
||||
frame->used, frame->width, frame->height,
|
||||
fourcc_to_string(frame->format, fourcc_str, 8),
|
||||
frame->stride, frame->online, frame->key,
|
||||
now - frame->grab_ts, (last_ts ? now - last_ts : 0));
|
||||
US_LOG_VERBOSE("Frame: %s - %ux%u -- online=%d, key=%d, kr=%d, gop=%u, latency=%.3Lf, backlog=%.3Lf, size=%zu",
|
||||
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->used);
|
||||
last_ts = now;
|
||||
|
||||
LOG_DEBUG(" grab_ts=%.3Lf, encode_begin_ts=%.3Lf, encode_end_ts=%.3Lf",
|
||||
frame->grab_ts, frame->encode_begin_ts, frame->encode_end_ts);
|
||||
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);
|
||||
|
||||
if (now_second != fps_second) {
|
||||
fps = fps_accum;
|
||||
fps_accum = 0;
|
||||
fps_second = now_second;
|
||||
LOG_PERF_FPS("A new second has come; captured_fps=%u", fps);
|
||||
US_LOG_PERF_FPS("A new second has come; captured_fps=%u", fps);
|
||||
}
|
||||
fps_accum += 1;
|
||||
|
||||
if (ctx->v_output) {
|
||||
if (ctx->v_output != NULL) {
|
||||
ctx->write(ctx->v_output, frame);
|
||||
}
|
||||
|
||||
@@ -295,28 +282,21 @@ static int _dump_sink(
|
||||
}
|
||||
}
|
||||
|
||||
int retval = 0;
|
||||
goto ok;
|
||||
retval = 0;
|
||||
|
||||
error:
|
||||
retval = -1;
|
||||
|
||||
ok:
|
||||
if (sink) {
|
||||
memsink_destroy(sink);
|
||||
}
|
||||
frame_destroy(frame);
|
||||
|
||||
LOG_INFO("Bye-bye");
|
||||
return retval;
|
||||
error:
|
||||
US_DELETE(sink, us_memsink_destroy);
|
||||
us_frame_destroy(frame);
|
||||
US_LOG_INFO("Bye-bye");
|
||||
return retval;
|
||||
}
|
||||
|
||||
static void _help(FILE *fp) {
|
||||
# define SAY(_msg, ...) fprintf(fp, _msg "\n", ##__VA_ARGS__)
|
||||
SAY("\nuStreamer-dump - Dump uStreamer's memory sink to file");
|
||||
SAY("═════════════════════════════════════════════════════");
|
||||
SAY("Version: %s; license: GPLv3", VERSION);
|
||||
SAY("Copyright (C) 2018-2021 Maxim Devaev <mdevaev@gmail.com>\n");
|
||||
SAY("Version: %s; license: GPLv3", US_VERSION);
|
||||
SAY("Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com>\n");
|
||||
SAY("Example:");
|
||||
SAY("════════");
|
||||
SAY(" ustreamer-dump --sink test --output - \\");
|
||||
@@ -329,12 +309,13 @@ static void _help(FILE *fp) {
|
||||
SAY(" -j|--output-json ──────── Format output as JSON. Required option --output. Default: disabled.\n");
|
||||
SAY(" -c|--count <N> ───────── Limit the number of frames. Default: 0 (infinite).\n");
|
||||
SAY(" -i|--interval <sec> ───── Delay between reading frames (float). Default: 0.\n");
|
||||
SAY(" -k|--key-required ─────── Request keyframe from the sink. Default: disabled.\n");
|
||||
SAY("Logging options:");
|
||||
SAY("════════════════");
|
||||
SAY(" --log-level <N> ──── Verbosity level of messages from 0 (info) to 3 (debug).");
|
||||
SAY(" Enabling debugging messages can slow down the program.");
|
||||
SAY(" Available levels: 0 (info), 1 (performance), 2 (verbose), 3 (debug).");
|
||||
SAY(" Default: %d.\n", us_log_level);
|
||||
SAY(" Default: %d.\n", us_g_log_level);
|
||||
SAY(" --perf ───────────── Enable performance messages (same as --log-level=1). Default: disabled.\n");
|
||||
SAY(" --verbose ────────── Enable verbose messages and lower (same as --log-level=2). Default: disabled.\n");
|
||||
SAY(" --debug ──────────── Enable debug messages and lower (same as --log-level=3). Default: disabled.\n");
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPG-HTTP streamer. #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2021 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,21 +22,16 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdio.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include <IL/OMX_IVCommon.h>
|
||||
#include <IL/OMX_Core.h>
|
||||
#include <IL/OMX_Image.h>
|
||||
|
||||
#include "../../../libs/tools.h"
|
||||
#include "../../../libs/logging.h"
|
||||
#define US_ARRAY_LEN(x_array) (sizeof(x_array) / sizeof((x_array)[0]))
|
||||
|
||||
|
||||
#define LOG_ERROR_OMX(_error, _msg, ...) { \
|
||||
LOG_ERROR(_msg ": %s", ##__VA_ARGS__, omx_error_to_string(_error)); \
|
||||
#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]; \
|
||||
__VA_ARGS__ \
|
||||
} \
|
||||
}
|
||||
|
||||
|
||||
const char *omx_error_to_string(OMX_ERRORTYPE error);
|
||||
const char *omx_state_to_string(OMX_STATETYPE state);
|
||||
@@ -1,8 +1,8 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPG-HTTP streamer. #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2021 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,27 +41,27 @@ 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 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)) {
|
||||
A_REALLOC(*encoded, encoded_size);
|
||||
US_REALLOC(*encoded, encoded_size);
|
||||
if (allocated) {
|
||||
*allocated = encoded_size;
|
||||
}
|
||||
}
|
||||
|
||||
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_index = 0, encoded_index = 0; data_index < size;) {
|
||||
# define OCTET(_name) uint _name = (data_index < size ? (u8)data[data_index++] : 0)
|
||||
OCTET(octet_a);
|
||||
OCTET(octet_b);
|
||||
OCTET(octet_c);
|
||||
# undef OCTET
|
||||
|
||||
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]
|
||||
ENCODE(3);
|
||||
@@ -64,7 +71,7 @@ void base64_encode(const uint8_t *data, size_t size, char **encoded, size_t *all
|
||||
# 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] = '=';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPG-HTTP streamer. #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2021 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 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);
|
||||
|
||||
1164
src/libs/capture.c
Normal file
1164
src/libs/capture.c
Normal file
File diff suppressed because it is too large
Load Diff
139
src/libs/capture.h
Normal file
139
src/libs/capture.h
Normal file
@@ -0,0 +1,139 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# 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"
|
||||
|
||||
|
||||
#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, RGB565, RGB24, BGR24, 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;
|
||||
uint width;
|
||||
uint height;
|
||||
uint format;
|
||||
uint stride;
|
||||
float hz;
|
||||
uint hw_fps;
|
||||
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_reported;
|
||||
} us_capture_runtime_s;
|
||||
|
||||
typedef enum {
|
||||
CTL_MODE_NONE = 0,
|
||||
CTL_MODE_VALUE,
|
||||
CTL_MODE_AUTO,
|
||||
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;
|
||||
|
||||
typedef struct {
|
||||
char *path;
|
||||
uint input;
|
||||
uint width;
|
||||
uint height;
|
||||
uint format;
|
||||
|
||||
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;
|
||||
uint desired_fps;
|
||||
uz min_frame_size;
|
||||
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(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);
|
||||
35
src/libs/const.h
Normal file
35
src/libs/const.h
Normal file
@@ -0,0 +1,35 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# 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"
|
||||
|
||||
|
||||
#define US_VERSION_MAJOR 6
|
||||
#define US_VERSION_MINOR 9
|
||||
|
||||
#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 ((uint)(US_VERSION_MAJOR * 1000 + US_VERSION_MINOR))
|
||||
740
src/libs/drm/drm.c
Normal file
740
src/libs/drm/drm.c
Normal file
@@ -0,0 +1,740 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# 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>
|
||||
#include <sys/sysmacros.h>
|
||||
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#include <xf86drm.h>
|
||||
#include <xf86drmMode.h>
|
||||
#include <drm_fourcc.h>
|
||||
#include <libdrm/drm.h>
|
||||
|
||||
#include "../types.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, 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->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->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;
|
||||
|
||||
assert(run->fd < 0);
|
||||
|
||||
switch (_drm_check_status(drm)) {
|
||||
case 0: break;
|
||||
case -2: goto unplugged;
|
||||
default: goto error;
|
||||
}
|
||||
|
||||
_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 -2: 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, &run->conn_id, 1, &run->mode) < 0) {
|
||||
_LOG_PERROR("Can't set CRTC");
|
||||
goto error;
|
||||
}
|
||||
|
||||
run->opened_for_stub = (stub > 0);
|
||||
run->exposing_dma_fd = -1;
|
||||
run->unplugged_reported = false;
|
||||
_LOG_INFO("Opened for %s ...", (run->opened_for_stub ? "STUB" : "DMA"));
|
||||
return stub;
|
||||
|
||||
error:
|
||||
us_drm_close(drm);
|
||||
return -1;
|
||||
|
||||
unplugged:
|
||||
if (!run->unplugged_reported) {
|
||||
_LOG_ERROR("Display is not plugged");
|
||||
run->unplugged_reported = true;
|
||||
}
|
||||
us_drm_close(drm);
|
||||
return -2;
|
||||
}
|
||||
|
||||
void us_drm_close(us_drm_s *drm) {
|
||||
us_drm_runtime_s *const run = drm->run;
|
||||
|
||||
if (run->exposing_dma_fd >= 0) {
|
||||
// Нужно подождать, пока dma_fd не освободится, прежде чем прерывать процесс.
|
||||
// Просто на всякий случай.
|
||||
assert(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->has_vsync = true;
|
||||
run->stub_n_buf = 0;
|
||||
|
||||
if (say) {
|
||||
_LOG_INFO("Closed");
|
||||
}
|
||||
}
|
||||
|
||||
int us_drm_dpms_power_off(us_drm_s *drm) {
|
||||
assert(drm->run->fd >= 0);
|
||||
switch (_drm_check_status(drm)) {
|
||||
case 0: break;
|
||||
case -2: 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;
|
||||
|
||||
assert(run->fd >= 0);
|
||||
|
||||
switch (_drm_check_status(drm)) {
|
||||
case 0: break;
|
||||
case -2: return -2;
|
||||
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;
|
||||
|
||||
assert(run->fd >= 0);
|
||||
assert(run->opened_for_stub);
|
||||
|
||||
switch (_drm_check_status(drm)) {
|
||||
case 0: break;
|
||||
case -2: return -2;
|
||||
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: {
|
||||
assert(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 SIGNAL >");
|
||||
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];
|
||||
|
||||
assert(run->fd >= 0);
|
||||
assert(!run->opened_for_stub);
|
||||
|
||||
switch (_drm_check_status(drm)) {
|
||||
case 0: break;
|
||||
case -2: return -2;
|
||||
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' ? -2 : 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;
|
||||
}
|
||||
|
||||
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 -2;
|
||||
}
|
||||
|
||||
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 (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);
|
||||
}
|
||||
assert(best == NULL || best->hdisplay > 0);
|
||||
assert(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, 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;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPG-HTTP streamer. #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2021 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,50 +22,71 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include <linux/videodev2.h>
|
||||
#include <xf86drmMode.h>
|
||||
|
||||
#include <interface/mmal/mmal.h>
|
||||
#include <interface/mmal/mmal_format.h>
|
||||
#include <interface/mmal/util/mmal_default_components.h>
|
||||
#include <interface/mmal/util/mmal_component_wrapper.h>
|
||||
#include <interface/mmal/util/mmal_util_params.h>
|
||||
#include <interface/vcsm/user-vcsm.h>
|
||||
#include "../types.h"
|
||||
#include "../frame.h"
|
||||
#include "../frametext.h"
|
||||
#include "../capture.h"
|
||||
|
||||
#include "../../libs/tools.h"
|
||||
#include "../../libs/logging.h"
|
||||
#include "../../libs/frame.h"
|
||||
#include "../encoders/omx/vcos.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 {
|
||||
unsigned bitrate; // Kbit-per-sec
|
||||
unsigned gop; // Interval between keyframes
|
||||
unsigned fps;
|
||||
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;
|
||||
|
||||
MMAL_WRAPPER_T *wrapper;
|
||||
MMAL_PORT_T *input_port;
|
||||
MMAL_PORT_T *output_port;
|
||||
VCOS_SEMAPHORE_T handler_sem;
|
||||
bool i_handler_sem;
|
||||
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;
|
||||
bool opened_for_stub;
|
||||
bool has_vsync;
|
||||
int exposing_dma_fd;
|
||||
uint stub_n_buf;
|
||||
bool unplugged_reported;
|
||||
us_frametext_s *ft;
|
||||
} us_drm_runtime_s;
|
||||
|
||||
int last_online;
|
||||
typedef struct {
|
||||
char *path;
|
||||
char *port;
|
||||
uint timeout;
|
||||
|
||||
unsigned width;
|
||||
unsigned height;
|
||||
unsigned format;
|
||||
unsigned stride;
|
||||
bool zero_copy;
|
||||
bool ready;
|
||||
} h264_encoder_s;
|
||||
us_drm_runtime_s *run;
|
||||
} us_drm_s;
|
||||
|
||||
|
||||
h264_encoder_s *h264_encoder_init(unsigned bitrate, unsigned gop, unsigned fps);
|
||||
void h264_encoder_destroy(h264_encoder_s *enc);
|
||||
us_drm_s *us_drm_init(void);
|
||||
void us_drm_destroy(us_drm_s *drm);
|
||||
|
||||
bool h264_encoder_is_prepared_for(h264_encoder_s *enc, const frame_s *frame, bool zero_copy);
|
||||
int h264_encoder_prepare(h264_encoder_s *enc, const frame_s *frame, bool zero_copy);
|
||||
int h264_encoder_compress(h264_encoder_s *enc, const frame_s *src, int src_vcsm_handle, frame_s *dest, bool force_key);
|
||||
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);
|
||||
@@ -1,8 +1,8 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPG-HTTP streamer. #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2021 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,76 @@
|
||||
|
||||
#include "frame.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
frame_s *frame_init(void) {
|
||||
frame_s *frame;
|
||||
A_CALLOC(frame, 1);
|
||||
frame_realloc_data(frame, 512 * 1024);
|
||||
#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);
|
||||
frame->dma_fd = -1;
|
||||
return frame;
|
||||
}
|
||||
|
||||
void frame_destroy(frame_s *frame) {
|
||||
if (frame->data) {
|
||||
free(frame->data);
|
||||
}
|
||||
void us_frame_destroy(us_frame_s *frame) {
|
||||
US_DELETE(frame->data, free);
|
||||
free(frame);
|
||||
}
|
||||
|
||||
void frame_realloc_data(frame_s *frame, size_t size) {
|
||||
void us_frame_realloc_data(us_frame_s *frame, uz size) {
|
||||
if (frame->allocated < size) {
|
||||
A_REALLOC(frame->data, size);
|
||||
US_REALLOC(frame->data, size);
|
||||
frame->allocated = size;
|
||||
}
|
||||
}
|
||||
|
||||
void frame_set_data(frame_s *frame, const uint8_t *data, size_t size) {
|
||||
frame_realloc_data(frame, 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 frame_append_data(frame_s *frame, const uint8_t *data, size_t size) {
|
||||
size_t new_used = frame->used + size;
|
||||
frame_realloc_data(frame, new_used);
|
||||
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;
|
||||
}
|
||||
|
||||
void frame_copy(const frame_s *src, frame_s *dest) {
|
||||
frame_set_data(dest, src->data, src->used);
|
||||
FRAME_COPY_META(src, dest);
|
||||
void us_frame_copy(const us_frame_s *src, us_frame_s *dest) {
|
||||
us_frame_set_data(dest, src->data, src->used);
|
||||
US_FRAME_COPY_META(src, dest);
|
||||
}
|
||||
|
||||
bool frame_compare(const frame_s *a, const frame_s *b) {
|
||||
bool us_frame_compare(const us_frame_s *a, const us_frame_s *b) {
|
||||
return (
|
||||
a->allocated && b->allocated
|
||||
&& FRAME_COMPARE_META_USED_NOTS(a, b)
|
||||
&& US_FRAME_COMPARE_GEOMETRY(a, b)
|
||||
&& !memcmp(a->data, b->data, b->used)
|
||||
);
|
||||
}
|
||||
|
||||
unsigned frame_get_padding(const 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_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_BGR24:
|
||||
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 pixelformat");
|
||||
default: assert(0 && "Unknown format");
|
||||
}
|
||||
if (bytes_per_pixel > 0 && frame->stride > frame->width) {
|
||||
return (frame->stride - frame->width * bytes_per_pixel);
|
||||
@@ -88,13 +99,17 @@ unsigned frame_get_padding(const frame_s *frame) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *fourcc_to_string(unsigned format, char *buf, size_t size) {
|
||||
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) {
|
||||
assert(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';
|
||||
|
||||
132
src/libs/frame.h
132
src/libs/frame.h
@@ -1,8 +1,8 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPG-HTTP streamer. #
|
||||
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018-2021 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,81 +22,87 @@
|
||||
|
||||
#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"
|
||||
|
||||
|
||||
typedef struct {
|
||||
uint8_t *data;
|
||||
size_t used;
|
||||
size_t allocated;
|
||||
u8 *data;
|
||||
uz used;
|
||||
uz allocated;
|
||||
int dma_fd;
|
||||
|
||||
unsigned width;
|
||||
unsigned height;
|
||||
unsigned format;
|
||||
unsigned stride;
|
||||
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;
|
||||
|
||||
bool online;
|
||||
bool key;
|
||||
|
||||
long double grab_ts;
|
||||
long double encode_begin_ts;
|
||||
long double encode_end_ts;
|
||||
} frame_s;
|
||||
ldf grab_ts;
|
||||
ldf encode_begin_ts;
|
||||
ldf encode_end_ts;
|
||||
} us_frame_s;
|
||||
|
||||
|
||||
#define FRAME_COPY_META(_src, _dest) { \
|
||||
_dest->width = _src->width; \
|
||||
_dest->height = _src->height; \
|
||||
_dest->format = _src->format; \
|
||||
_dest->stride = _src->stride; \
|
||||
_dest->online = _src->online; \
|
||||
_dest->key = _src->key; \
|
||||
_dest->grab_ts = _src->grab_ts; \
|
||||
_dest->encode_begin_ts = _src->encode_begin_ts; \
|
||||
_dest->encode_end_ts = _src->encode_end_ts; \
|
||||
#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; \
|
||||
}
|
||||
|
||||
static inline void frame_copy_meta(const frame_s *src, frame_s *dest) {
|
||||
FRAME_COPY_META(src, dest);
|
||||
}
|
||||
|
||||
#define FRAME_COMPARE_META_USED_NOTS(_a, _b) ( \
|
||||
_a->used == _b->used \
|
||||
&& _a->width == _b->width \
|
||||
&& _a->height == _b->height \
|
||||
&& _a->format == _b->format \
|
||||
&& _a->stride == _b->stride \
|
||||
&& _a->online == _b->online \
|
||||
&& _a->key == _b->key \
|
||||
#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 \
|
||||
)
|
||||
|
||||
|
||||
frame_s *frame_init(void);
|
||||
void frame_destroy(frame_s *frame);
|
||||
|
||||
void frame_realloc_data(frame_s *frame, size_t size);
|
||||
void frame_set_data(frame_s *frame, const uint8_t *data, size_t size);
|
||||
void frame_append_data(frame_s *frame, const uint8_t *data, size_t size);
|
||||
|
||||
void frame_copy(const frame_s *src, frame_s *dest);
|
||||
bool frame_compare(const frame_s *a, const frame_s *b);
|
||||
|
||||
unsigned frame_get_padding(const frame_s *frame);
|
||||
|
||||
const char *fourcc_to_string(unsigned format, char *buf, size_t size);
|
||||
|
||||
static inline bool is_jpeg(unsigned format) {
|
||||
return (format == V4L2_PIX_FMT_JPEG || format == V4L2_PIX_FMT_MJPEG);
|
||||
static inline void us_frame_encoding_begin(const us_frame_s *src, us_frame_s *dest, uint format) {
|
||||
assert(src->used > 0);
|
||||
US_FRAME_COPY_META(src, dest);
|
||||
dest->encode_begin_ts = us_get_now_monotonic();
|
||||
dest->format = format;
|
||||
dest->stride = 0;
|
||||
dest->used = 0;
|
||||
}
|
||||
|
||||
static inline void us_frame_encoding_end(us_frame_s *dest) {
|
||||
assert(dest->used > 0);
|
||||
dest->encode_end_ts = us_get_now_monotonic();
|
||||
}
|
||||
|
||||
|
||||
us_frame_s *us_frame_init(void);
|
||||
void us_frame_destroy(us_frame_s *frame);
|
||||
|
||||
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);
|
||||
|
||||
uint us_frame_get_padding(const us_frame_s *frame);
|
||||
|
||||
bool us_is_jpeg(uint format);
|
||||
const char *us_fourcc_to_string(uint format, char *buf, uz size);
|
||||
|
||||
186
src/libs/frametext.c
Normal file
186
src/libs/frametext.c
Normal file
@@ -0,0 +1,186 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# 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) {
|
||||
assert(width > 0);
|
||||
assert(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);
|
||||
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;
|
||||
}
|
||||
uint scale_x = frame->width / block_width / 2;
|
||||
uint scale_y = frame->height / block_height / 3;
|
||||
if (scale_x < scale_y / 1.5) {
|
||||
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);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user