mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-13 17:50:29 +08:00
进一步移植:能够打包 exe 运行
This commit is contained in:
parent
5bf2466037
commit
45b394185a
6
.gitignore
vendored
6
.gitignore
vendored
@ -19,7 +19,7 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
*.swp
|
*.swp
|
||||||
/venv/
|
/venv/
|
||||||
.vscode/settings.j/son
|
.vscode/settings.json
|
||||||
kvmd_config/
|
kvmd_config/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
kvmd_data/run/kvmd/*
|
kvmd_data/run/kvmd/*
|
||||||
@ -30,3 +30,7 @@ kvmd-launcher.dist
|
|||||||
kvmd-launcher.onefile-build
|
kvmd-launcher.onefile-build
|
||||||
ustreamer/
|
ustreamer/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
build/
|
||||||
|
*/dist/*
|
||||||
|
*/build/*
|
||||||
|
*.spec
|
||||||
366
Makefile
366
Makefile
@ -1,366 +0,0 @@
|
|||||||
-include config.mk
|
|
||||||
|
|
||||||
TESTENV_IMAGE ?= kvmd-testenv
|
|
||||||
TESTENV_HID ?= /dev/ttyS10
|
|
||||||
TESTENV_VIDEO ?= /dev/video0
|
|
||||||
TESTENV_GPIO ?= /dev/gpiochip0
|
|
||||||
TESTENV_RELAY ?= $(if $(shell ls /dev/hidraw0 2>/dev/null || true),/dev/hidraw0,)
|
|
||||||
|
|
||||||
LIBGPIOD_VERSION ?= 1.6.3
|
|
||||||
|
|
||||||
USTREAMER_MIN_VERSION ?= $(shell grep -o 'ustreamer>=[^"]\+' PKGBUILD | sed 's/ustreamer>=//g')
|
|
||||||
|
|
||||||
DEFAULT_PLATFORM ?= v2-hdmiusb-rpi4
|
|
||||||
|
|
||||||
DOCKER ?= docker
|
|
||||||
|
|
||||||
|
|
||||||
# =====
|
|
||||||
define optbool
|
|
||||||
$(filter $(shell echo $(1) | tr A-Z a-z),yes on 1)
|
|
||||||
endef
|
|
||||||
|
|
||||||
|
|
||||||
# =====
|
|
||||||
all:
|
|
||||||
@ echo "Useful commands:"
|
|
||||||
@ echo " make # Print this help"
|
|
||||||
@ echo " make testenv # Build test environment"
|
|
||||||
@ echo " make tox # Run tests and linters"
|
|
||||||
@ echo " make tox E=pytest # Run selected test environment"
|
|
||||||
@ echo " make gpio # Create gpio mockup"
|
|
||||||
@ echo " make run # Run kvmd"
|
|
||||||
@ echo " make run CMD=... # Run specified command inside kvmd environment"
|
|
||||||
@ echo " make run-cfg # Run kvmd -m"
|
|
||||||
@ echo " make run-ipmi # Run kvmd-ipmi"
|
|
||||||
@ echo " make run-ipmi CMD=... # Run specified command inside kvmd-ipmi environment"
|
|
||||||
@ echo " make run-vnc # Run kvmd-vnc"
|
|
||||||
@ echo " make run-vnc CMD=... # Run specified command inside kvmd-vnc environment"
|
|
||||||
@ echo " make regen # Regen some sources like keymap"
|
|
||||||
@ echo " make bump # Bump minor version"
|
|
||||||
@ echo " make bump V=major # Bump major version"
|
|
||||||
@ echo " make release # Publish the new release (include bump minor)"
|
|
||||||
@ echo " make clean # Remove garbage"
|
|
||||||
@ echo " make clean-all # Remove garbage and test results"
|
|
||||||
@ echo
|
|
||||||
@ echo "Also you can add option NC=1 to rebuild docker test environment"
|
|
||||||
|
|
||||||
|
|
||||||
testenv:
|
|
||||||
$(DOCKER) build \
|
|
||||||
$(if $(call optbool,$(NC)),--no-cache,) \
|
|
||||||
--rm \
|
|
||||||
--tag $(TESTENV_IMAGE) \
|
|
||||||
--build-arg LIBGPIOD_VERSION=$(LIBGPIOD_VERSION) \
|
|
||||||
--build-arg USTREAMER_MIN_VERSION=$(USTREAMER_MIN_VERSION) \
|
|
||||||
-f testenv/Dockerfile .
|
|
||||||
test -d testenv/.ssl || $(DOCKER) run --rm \
|
|
||||||
--volume `pwd`:/src:ro \
|
|
||||||
--volume `pwd`/testenv:/src/testenv:rw \
|
|
||||||
-t $(TESTENV_IMAGE) bash -c " \
|
|
||||||
groupadd kvmd-nginx \
|
|
||||||
&& groupadd kvmd-vnc \
|
|
||||||
&& /src/scripts/kvmd-gencert --do-the-thing \
|
|
||||||
&& /src/scripts/kvmd-gencert --do-the-thing --vnc \
|
|
||||||
&& chown -R root:root /etc/kvmd/{nginx,vnc}/ssl \
|
|
||||||
&& chmod 664 /etc/kvmd/{nginx,vnc}/ssl/* \
|
|
||||||
&& chmod 775 /etc/kvmd/{nginx,vnc}/ssl \
|
|
||||||
&& mkdir /src/testenv/.ssl \
|
|
||||||
&& mv /etc/kvmd/nginx/ssl /src/testenv/.ssl/nginx \
|
|
||||||
&& mv /etc/kvmd/vnc/ssl /src/testenv/.ssl/vnc \
|
|
||||||
"
|
|
||||||
|
|
||||||
|
|
||||||
tox: testenv
|
|
||||||
time $(DOCKER) run --rm \
|
|
||||||
--volume `pwd`:/src:ro \
|
|
||||||
--volume `pwd`/testenv:/src/testenv:rw \
|
|
||||||
--volume `pwd`/testenv/tests:/src/testenv/tests:ro \
|
|
||||||
--volume `pwd`/extras:/usr/share/kvmd/extras:ro \
|
|
||||||
--volume `pwd`/configs:/usr/share/kvmd/configs.default:ro \
|
|
||||||
--volume `pwd`/contrib/keymaps:/usr/share/kvmd/keymaps:ro \
|
|
||||||
-t $(TESTENV_IMAGE) bash -c " \
|
|
||||||
cp -a /src/testenv/.ssl/nginx /etc/kvmd/nginx/ssl \
|
|
||||||
&& cp -a /src/testenv/.ssl/vnc /etc/kvmd/vnc/ssl \
|
|
||||||
&& cp /src/testenv/platform /usr/share/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.yaml /etc/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/*passwd /etc/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.secret /etc/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/main.yaml /etc/kvmd/main.yaml \
|
|
||||||
&& mkdir -p /etc/kvmd/override.d \
|
|
||||||
&& cp /src/testenv/$(if $(P),$(P),$(DEFAULT_PLATFORM)).override.yaml /etc/kvmd/override.yaml \
|
|
||||||
&& cd /src \
|
|
||||||
&& $(if $(CMD),$(CMD),tox -q -c testenv/tox.ini $(if $(E),-e $(E),-p auto)) \
|
|
||||||
"
|
|
||||||
|
|
||||||
|
|
||||||
$(TESTENV_GPIO):
|
|
||||||
test ! -e $(TESTENV_GPIO)
|
|
||||||
sudo modprobe gpio-mockup gpio_mockup_ranges=0,40
|
|
||||||
test -c $(TESTENV_GPIO)
|
|
||||||
|
|
||||||
|
|
||||||
run: testenv $(TESTENV_GPIO)
|
|
||||||
- $(DOCKER) run --rm --name kvmd \
|
|
||||||
--privileged \
|
|
||||||
--volume `pwd`/testenv/run:/run/kvmd:rw \
|
|
||||||
--volume `pwd`/testenv:/testenv:ro \
|
|
||||||
--volume `pwd`/kvmd:/kvmd:ro \
|
|
||||||
--volume `pwd`/testenv/env.py:/kvmd/env.py:ro \
|
|
||||||
--volume `pwd`/web:/usr/share/kvmd/web:ro \
|
|
||||||
--volume `pwd`/extras:/usr/share/kvmd/extras:ro \
|
|
||||||
--volume `pwd`/configs:/usr/share/kvmd/configs.default:ro \
|
|
||||||
--volume `pwd`/contrib/keymaps:/usr/share/kvmd/keymaps:ro \
|
|
||||||
--device $(TESTENV_VIDEO):$(TESTENV_VIDEO) \
|
|
||||||
--device $(TESTENV_GPIO):$(TESTENV_GPIO) \
|
|
||||||
$(if $(TESTENV_RELAY),--device $(TESTENV_RELAY):$(TESTENV_RELAY),) \
|
|
||||||
--publish 8080:8080/tcp \
|
|
||||||
--publish 4430:4430/tcp \
|
|
||||||
-it $(TESTENV_IMAGE) /bin/bash -c " \
|
|
||||||
mkdir -p /tmp/kvmd-nginx \
|
|
||||||
&& mount -t debugfs none /sys/kernel/debug \
|
|
||||||
&& test -d /sys/kernel/debug/gpio-mockup/`basename $(TESTENV_GPIO)`/ || (echo \"Missing GPIO mockup\" && exit 1) \
|
|
||||||
&& (socat PTY,link=$(TESTENV_HID) PTY,link=/dev/ttyS11 &) \
|
|
||||||
&& cp -r /usr/share/kvmd/configs.default/nginx/* /etc/kvmd/nginx \
|
|
||||||
&& cp -a /testenv/.ssl/nginx /etc/kvmd/nginx/ssl \
|
|
||||||
&& cp -a /testenv/.ssl/vnc /etc/kvmd/vnc/ssl \
|
|
||||||
&& cp /testenv/platform /usr/share/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.yaml /etc/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/*passwd /etc/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.secret /etc/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/main/$(if $(P),$(P),$(DEFAULT_PLATFORM)).yaml /etc/kvmd/main.yaml \
|
|
||||||
&& ln -s /testenv/web.css /etc/kvmd/web.css \
|
|
||||||
&& mkdir -p /etc/kvmd/override.d \
|
|
||||||
&& cp /testenv/$(if $(P),$(P),$(DEFAULT_PLATFORM)).override.yaml /etc/kvmd/override.yaml \
|
|
||||||
&& python -m kvmd.apps.ngxmkconf /etc/kvmd/nginx/nginx.conf.mako /etc/kvmd/nginx/nginx.conf \
|
|
||||||
&& nginx -c /etc/kvmd/nginx/nginx.conf -g 'user http; error_log stderr;' \
|
|
||||||
&& ln -s $(TESTENV_VIDEO) /dev/kvmd-video \
|
|
||||||
&& ln -s $(TESTENV_GPIO) /dev/kvmd-gpio \
|
|
||||||
&& $(if $(CMD),$(CMD),python -m kvmd.apps.kvmd --run) \
|
|
||||||
"
|
|
||||||
|
|
||||||
|
|
||||||
run-cfg: testenv
|
|
||||||
- $(DOCKER) run --rm --name kvmd-cfg \
|
|
||||||
--volume `pwd`/testenv/run:/run/kvmd:rw \
|
|
||||||
--volume `pwd`/testenv:/testenv:ro \
|
|
||||||
--volume `pwd`/kvmd:/kvmd:ro \
|
|
||||||
--volume `pwd`/extras:/usr/share/kvmd/extras:ro \
|
|
||||||
--volume `pwd`/configs:/usr/share/kvmd/configs.default:ro \
|
|
||||||
--volume `pwd`/contrib/keymaps:/usr/share/kvmd/keymaps:ro \
|
|
||||||
-it $(TESTENV_IMAGE) /bin/bash -c " \
|
|
||||||
cp -a /testenv/.ssl/nginx /etc/kvmd/nginx/ssl \
|
|
||||||
&& cp -a /testenv/.ssl/vnc /etc/kvmd/vnc/ssl \
|
|
||||||
&& cp /testenv/platform /usr/share/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.yaml /etc/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/*passwd /etc/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.secret /etc/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/main.yaml /etc/kvmd/main.yaml \
|
|
||||||
&& mkdir -p /etc/kvmd/override.d \
|
|
||||||
&& cp /testenv/$(if $(P),$(P),$(DEFAULT_PLATFORM)).override.yaml /etc/kvmd/override.yaml \
|
|
||||||
&& $(if $(CMD),$(CMD),python -m kvmd.apps.kvmd -m) \
|
|
||||||
"
|
|
||||||
|
|
||||||
|
|
||||||
run-ipmi: testenv
|
|
||||||
- $(DOCKER) run --rm --name kvmd-ipmi \
|
|
||||||
--volume `pwd`/testenv/run:/run/kvmd:rw \
|
|
||||||
--volume `pwd`/testenv:/testenv:ro \
|
|
||||||
--volume `pwd`/kvmd:/kvmd:ro \
|
|
||||||
--volume `pwd`/extras:/usr/share/kvmd/extras:ro \
|
|
||||||
--volume `pwd`/configs:/usr/share/kvmd/configs.default:ro \
|
|
||||||
--volume `pwd`/contrib/keymaps:/usr/share/kvmd/keymaps:ro \
|
|
||||||
--publish 6230:623/udp \
|
|
||||||
-it $(TESTENV_IMAGE) /bin/bash -c " \
|
|
||||||
cp -a /testenv/.ssl/nginx /etc/kvmd/nginx/ssl \
|
|
||||||
&& cp -a /testenv/.ssl/vnc /etc/kvmd/vnc/ssl \
|
|
||||||
&& cp /testenv/platform /usr/share/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.yaml /etc/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/*passwd /etc/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.secret /etc/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/main.yaml /etc/kvmd/main.yaml \
|
|
||||||
&& mkdir -p /etc/kvmd/override.d \
|
|
||||||
&& cp /testenv/$(if $(P),$(P),$(DEFAULT_PLATFORM)).override.yaml /etc/kvmd/override.yaml \
|
|
||||||
&& $(if $(CMD),$(CMD),python -m kvmd.apps.ipmi --run) \
|
|
||||||
"
|
|
||||||
|
|
||||||
|
|
||||||
run-vnc: testenv
|
|
||||||
- $(DOCKER) run --rm --name kvmd-vnc \
|
|
||||||
--volume `pwd`/testenv/run:/run/kvmd:rw \
|
|
||||||
--volume `pwd`/testenv:/testenv:ro \
|
|
||||||
--volume `pwd`/kvmd:/kvmd:ro \
|
|
||||||
--volume `pwd`/extras:/usr/share/kvmd/extras:ro \
|
|
||||||
--volume `pwd`/configs:/usr/share/kvmd/configs.default:ro \
|
|
||||||
--volume `pwd`/contrib/keymaps:/usr/share/kvmd/keymaps:ro \
|
|
||||||
--publish 5900:5900/tcp \
|
|
||||||
-it $(TESTENV_IMAGE) /bin/bash -c " \
|
|
||||||
cp -a /testenv/.ssl/nginx /etc/kvmd/nginx/ssl \
|
|
||||||
&& cp -a /testenv/.ssl/vnc /etc/kvmd/vnc/ssl \
|
|
||||||
&& cp /testenv/platform /usr/share/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.yaml /etc/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/*passwd /etc/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.secret /etc/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/main.yaml /etc/kvmd/main.yaml \
|
|
||||||
&& mkdir -p /etc/kvmd/override.d \
|
|
||||||
&& cp /testenv/$(if $(P),$(P),$(DEFAULT_PLATFORM)).override.yaml /etc/kvmd/override.yaml \
|
|
||||||
&& $(if $(CMD),$(CMD),python -m kvmd.apps.vnc --run) \
|
|
||||||
"
|
|
||||||
|
|
||||||
|
|
||||||
regen: keymap pug
|
|
||||||
|
|
||||||
|
|
||||||
keymap: testenv
|
|
||||||
$(DOCKER) run --user `id -u`:`id -g` --rm \
|
|
||||||
--volume `pwd`:/src \
|
|
||||||
-it $(TESTENV_IMAGE) bash -c "cd src \
|
|
||||||
&& ./genmap.py keymap.csv kvmd/keyboard/mappings.py.mako kvmd/keyboard/mappings.py \
|
|
||||||
&& ./genmap.py keymap.csv hid/arduino/lib/drivers/usb-keymap.h.mako hid/arduino/lib/drivers/usb-keymap.h \
|
|
||||||
&& ./genmap.py keymap.csv hid/arduino/lib/drivers-avr/ps2/keymap.h.mako hid/arduino/lib/drivers-avr/ps2/keymap.h \
|
|
||||||
&& ./genmap.py keymap.csv hid/pico/src/ph_usb_keymap.h.mako hid/pico/src/ph_usb_keymap.h \
|
|
||||||
"
|
|
||||||
|
|
||||||
|
|
||||||
pug: testenv
|
|
||||||
$(DOCKER) run --user `id -u`:`id -g` --rm \
|
|
||||||
--volume `pwd`:/src \
|
|
||||||
-it $(TESTENV_IMAGE) bash -c "cd src \
|
|
||||||
&& pug --pretty web/index.pug -o web \
|
|
||||||
&& pug --pretty web/login/index.pug -o web/login \
|
|
||||||
&& pug --pretty web/kvm/index.pug -o web/kvm \
|
|
||||||
&& pug --pretty web/ipmi/index.pug -o web/ipmi \
|
|
||||||
&& pug --pretty web/vnc/index.pug -o web/vnc \
|
|
||||||
"
|
|
||||||
|
|
||||||
|
|
||||||
release:
|
|
||||||
make clean
|
|
||||||
make tox
|
|
||||||
make clean
|
|
||||||
make push
|
|
||||||
make bump V=$(V)
|
|
||||||
make push
|
|
||||||
make clean
|
|
||||||
|
|
||||||
|
|
||||||
bump:
|
|
||||||
bumpversion $(if $(V),$(V),minor)
|
|
||||||
|
|
||||||
|
|
||||||
push:
|
|
||||||
git push
|
|
||||||
git push --tags
|
|
||||||
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -rf testenv/run/*.{pid,sock} build site dist pkg src v*.tar.gz *.pkg.tar.{xz,zst} *.egg-info kvmd-*.tar.gz
|
|
||||||
find kvmd testenv/tests -name __pycache__ | xargs rm -rf
|
|
||||||
make -C hid/arduino clean
|
|
||||||
make -C hid/pico clean
|
|
||||||
|
|
||||||
|
|
||||||
clean-all: testenv clean
|
|
||||||
make -C hid/arduino clean-all
|
|
||||||
make -C hid/pico clean-all
|
|
||||||
- $(DOCKER) run --rm \
|
|
||||||
--volume `pwd`:/src \
|
|
||||||
-it $(TESTENV_IMAGE) bash -c "cd src && rm -rf testenv/{.ssl,.tox,.mypy_cache,.coverage}"
|
|
||||||
|
|
||||||
|
|
||||||
.PHONY: testenv
|
|
||||||
|
|
||||||
run-stage-0:
|
|
||||||
$(DOCKER) buildx build -t registry.cn-hangzhou.aliyuncs.com/silentwind/kvmd-stage-0 \
|
|
||||||
--allow security.insecure --progress plain \
|
|
||||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
|
||||||
-f build/Dockerfile-stage-0 . \
|
|
||||||
--push
|
|
||||||
$(DOCKER) buildx build -t silentwind0/kvmd-stage-0 \
|
|
||||||
--allow security.insecure --progress plain \
|
|
||||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
|
||||||
-f build/Dockerfile-stage-0 . \
|
|
||||||
--push
|
|
||||||
|
|
||||||
run-build-dev:
|
|
||||||
$(DOCKER) buildx build -t registry.cn-hangzhou.aliyuncs.com/silentwind/kvmd:dev \
|
|
||||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
|
||||||
--build-arg CACHEBUST=$(date +%s) --allow security.insecure \
|
|
||||||
-f build/Dockerfile . \
|
|
||||||
--push
|
|
||||||
$(DOCKER) buildx build -t silentwind0/kvmd:dev \
|
|
||||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
|
||||||
--build-arg CACHEBUST=$(date +%s) --allow security.insecure \
|
|
||||||
-f build/Dockerfile . \
|
|
||||||
--push
|
|
||||||
|
|
||||||
run-build-new:
|
|
||||||
$(DOCKER) buildx build -t registry.cn-hangzhou.aliyuncs.com/silentwind/kvmd:dev \
|
|
||||||
--platform linux/amd64 \
|
|
||||||
--build-arg CACHEBUST=$(date +%s) \
|
|
||||||
-f build/Dockerfile . \
|
|
||||||
--load
|
|
||||||
|
|
||||||
run-build-release:
|
|
||||||
$(DOCKER) buildx build -t registry.cn-hangzhou.aliyuncs.com/silentwind/kvmd \
|
|
||||||
--progress plain \
|
|
||||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
|
||||||
--build-arg CACHEBUST=$(date +%s) \
|
|
||||||
-f build/Dockerfile . \
|
|
||||||
--push
|
|
||||||
$(DOCKER) buildx build -t silentwind0/kvmd \
|
|
||||||
--progress plain \
|
|
||||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
|
||||||
--build-arg CACHEBUST=$(date +%s) \
|
|
||||||
-f build/Dockerfile . \
|
|
||||||
--push
|
|
||||||
|
|
||||||
run-nogpio: testenv
|
|
||||||
- $(DOCKER) run --rm --name kvmd \
|
|
||||||
--privileged \
|
|
||||||
--volume `pwd`/testenv/run:/run/kvmd:rw \
|
|
||||||
--volume `pwd`/testenv:/testenv:ro \
|
|
||||||
--volume `pwd`/kvmd:/kvmd:ro \
|
|
||||||
--volume `pwd`/testenv/env.py:/kvmd/env.py:ro \
|
|
||||||
--volume `pwd`/web:/usr/share/kvmd/web:ro \
|
|
||||||
--volume `pwd`/extras:/usr/share/kvmd/extras:ro \
|
|
||||||
--volume `pwd`/configs:/usr/share/kvmd/configs.default:ro \
|
|
||||||
--volume `pwd`/contrib/keymaps:/usr/share/kvmd/keymaps:ro \
|
|
||||||
--device $(TESTENV_VIDEO):$(TESTENV_VIDEO) \
|
|
||||||
$(if $(TESTENV_RELAY),--device $(TESTENV_RELAY):$(TESTENV_RELAY),) \
|
|
||||||
--publish 8080:8080/tcp \
|
|
||||||
--publish 4430:4430/tcp \
|
|
||||||
-it $(TESTENV_IMAGE) /bin/bash -c " \
|
|
||||||
mkdir -p /tmp/kvmd-nginx \
|
|
||||||
&& mount -t debugfs none /sys/kernel/debug \
|
|
||||||
&& (socat PTY,link=$(TESTENV_HID) PTY,link=/dev/ttyS11 &) \
|
|
||||||
&& cp -r /usr/share/kvmd/configs.default/nginx/* /etc/kvmd/nginx \
|
|
||||||
&& cp -a /testenv/.ssl/nginx /etc/kvmd/nginx/ssl \
|
|
||||||
&& cp -a /testenv/.ssl/vnc /etc/kvmd/vnc/ssl \
|
|
||||||
&& touch /etc/kvmd/.docker_flag \
|
|
||||||
&& cp /testenv/platform /usr/share/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.yaml /etc/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/*passwd /etc/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/*.secret /etc/kvmd \
|
|
||||||
&& cp /usr/share/kvmd/configs.default/kvmd/main.yaml /etc/kvmd/main.yaml \
|
|
||||||
&& ln -s /testenv/web.css /etc/kvmd/web.css \
|
|
||||||
&& mkdir -p /etc/kvmd/override.d \
|
|
||||||
&& cp /testenv/$(if $(P),$(P),$(DEFAULT_PLATFORM)).override.yaml /etc/kvmd/override.yaml \
|
|
||||||
&& python -m kvmd.apps.ngxmkconf /etc/kvmd/nginx/nginx.conf.mako /etc/kvmd/nginx/nginx.conf \
|
|
||||||
&& nginx -c /etc/kvmd/nginx/nginx.conf -g 'user http; error_log stderr;' \
|
|
||||||
&& $(if $(CMD),$(CMD),python -m kvmd.apps.kvmd --run) \
|
|
||||||
"
|
|
||||||
nuitka:
|
|
||||||
python3.11 -m nuitka kvmd-launcher.py --standalone --onefile --no-deployment-flag=self-execution --include-module=\
|
|
||||||
kvmd.plugins.auth.htpasswd,kvmd.plugins.auth.http,kvmd.plugins.auth.ldap,\
|
|
||||||
kvmd.plugins.auth.pam,kvmd.plugins.auth.radius,\
|
|
||||||
kvmd.plugins.hid.ch9329,kvmd.plugins.hid.bt,kvmd.plugins.hid.otg,\
|
|
||||||
kvmd.plugins.atx.disabled,kvmd.plugins.atx.gpio,\
|
|
||||||
kvmd.plugins.msd.disabled,kvmd.plugins.msd.otg,\
|
|
||||||
kvmd.plugins.ugpio.gpio,kvmd.plugins.ugpio.wol,kvmd.plugins.ugpio.cmd,\
|
|
||||||
kvmd.plugins.ugpio.ipmi,kvmd.plugins.ugpio.anelpwr,kvmd.plugins.ugpio.cmdret,\
|
|
||||||
kvmd.plugins.ugpio.extron,kvmd.plugins.ugpio.ezcoo,kvmd.plugins.ugpio.hidrelay,\
|
|
||||||
kvmd.plugins.ugpio.hue,kvmd.plugins.ugpio.locator,kvmd.plugins.ugpio.noyito,\
|
|
||||||
kvmd.plugins.ugpio.otgconf,kvmd.plugins.ugpio.pway,kvmd.plugins.ugpio.pwm,\
|
|
||||||
kvmd.plugins.ugpio.servo,kvmd.plugins.ugpio.tesmart,kvmd.plugins.ugpio.xh_hk4401,\
|
|
||||||
passlib.handlers.sha1_crypt,pygments.formatters.terminal
|
|
||||||
@ -1,11 +1,68 @@
|
|||||||
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
from kvmd.apps.kvmd import main as kvmd_main
|
from kvmd.apps.kvmd import main as kvmd_main
|
||||||
|
|
||||||
|
import fileinput
|
||||||
|
|
||||||
|
# 文件路径
|
||||||
|
file_path = '_internal/kvmd_data/etc/kvmd/kvmd_data/etc/kvmd/override.yaml'
|
||||||
|
|
||||||
|
# 使用fileinput.input进行原地编辑
|
||||||
|
|
||||||
|
|
||||||
|
def resource_path(relative_path):
|
||||||
|
if hasattr(sys, '_MEIPASS'):
|
||||||
|
base_path = sys._MEIPASS
|
||||||
|
else:
|
||||||
|
base_path = os.path.abspath(".")
|
||||||
|
return os.path.join(base_path, relative_path)
|
||||||
|
|
||||||
|
def replace_streamer_command(override_config_path):
|
||||||
|
lines_to_replace = [
|
||||||
|
" - \"C:/Users/mofen/miniconda3/python.exe\"\n",
|
||||||
|
" - \"ustreamer-win/ustreamer-win.py\"\n"
|
||||||
|
]
|
||||||
|
new_line = " - \"ustreamer-win.exe\"\n"
|
||||||
|
|
||||||
|
with open(override_config_path, 'r', encoding='utf-8') as file:
|
||||||
|
lines = file.readlines()
|
||||||
|
|
||||||
|
with open(override_config_path, 'w', encoding='utf-8') as file:
|
||||||
|
i = 0
|
||||||
|
while i < len(lines):
|
||||||
|
if lines[i] in lines_to_replace:
|
||||||
|
if i + 1 < len(lines) and lines[i + 1] == lines_to_replace[1]:
|
||||||
|
file.write(new_line)
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
file.write(lines[i])
|
||||||
|
i += 1
|
||||||
|
|
||||||
def start():
|
def start():
|
||||||
|
main_config_path = resource_path('kvmd_data/etc/kvmd/main.yaml')
|
||||||
|
override_config_path = resource_path('kvmd_data/etc/kvmd/override.yaml')
|
||||||
|
flag_path = resource_path('kvmd_data/run_flag')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if not os.path.exists(flag_path):
|
||||||
|
with fileinput.input(override_config_path, inplace=True) as file:
|
||||||
|
for line in file:
|
||||||
|
updated_line = line.replace('kvmd_data/', '_internal/kvmd_data/')
|
||||||
|
print(updated_line, end='')
|
||||||
|
with open(flag_path, 'w') as flag_file:
|
||||||
|
flag_file.write("1")
|
||||||
|
|
||||||
|
replace_streamer_command(override_config_path)
|
||||||
|
|
||||||
custom_argv = [
|
custom_argv = [
|
||||||
'kvmd',
|
'kvmd',
|
||||||
'-c', 'kvmd_data/etc/kvmd/main.yaml',
|
'-c',main_config_path,
|
||||||
'--run'
|
'--run'
|
||||||
]
|
]
|
||||||
kvmd_main(argv=custom_argv)
|
kvmd_main(argv=custom_argv)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
multiprocessing.freeze_support()
|
||||||
start()
|
start()
|
||||||
@ -53,7 +53,7 @@ class ExtrasInfoSubmanager(BaseInfoSubmanager):
|
|||||||
await sui.open()
|
await sui.open()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
if not os.path.exists("/etc/kvmd/.docker_flag") or not sys.platform.startswith('linux'):
|
if not os.path.exists("/etc/kvmd/.docker_flag") or not sys.platform.startswith('linux'):
|
||||||
get_logger(0).error("Can't open systemd bus to get extras state: %s", tools.efmt(ex))
|
get_logger(0).error("Can't open systemd bus to get extras state.")
|
||||||
sui = None
|
sui = None
|
||||||
try:
|
try:
|
||||||
extras: dict[str, dict] = {}
|
extras: dict[str, dict] = {}
|
||||||
|
|||||||
@ -83,9 +83,9 @@ class HwInfoSubmanager(BaseInfoSubmanager):
|
|||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"platform": {
|
"platform": {
|
||||||
"type": "rpi",
|
"type": "windows",
|
||||||
"base": base,
|
"base": base,
|
||||||
"serial": serial,
|
"serial": "windows1000000000",
|
||||||
**platform, # type: ignore
|
**platform, # type: ignore
|
||||||
},
|
},
|
||||||
"health": {
|
"health": {
|
||||||
@ -124,7 +124,7 @@ class HwInfoSubmanager(BaseInfoSubmanager):
|
|||||||
self.__dt_cache[name] = (await aiotools.read_file(path)).strip(" \t\r\n\0")
|
self.__dt_cache[name] = (await aiotools.read_file(path)).strip(" \t\r\n\0")
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
#get_logger(0).warn("Can't read DT %s from %s: %s", name, path, err)
|
#get_logger(0).warn("Can't read DT %s from %s: %s", name, path, err)
|
||||||
return None
|
return "windows"
|
||||||
return self.__dt_cache[name]
|
return self.__dt_cache[name]
|
||||||
|
|
||||||
async def __read_platform_file(self) -> dict:
|
async def __read_platform_file(self) -> dict:
|
||||||
@ -142,8 +142,8 @@ class HwInfoSubmanager(BaseInfoSubmanager):
|
|||||||
"board": parsed["PIKVM_BOARD"],
|
"board": parsed["PIKVM_BOARD"],
|
||||||
}
|
}
|
||||||
except Exception:
|
except Exception:
|
||||||
get_logger(0).exception("Can't read device model")
|
#get_logger(0).exception("Can't read device model")
|
||||||
return {"model": None, "video": None, "board": None}
|
return {"model": "V2", "video": "USB_VIDEO", "board": "Windows"}
|
||||||
|
|
||||||
async def __get_cpu_temp(self) -> (float | None):
|
async def __get_cpu_temp(self) -> (float | None):
|
||||||
temp_path = f"{env.SYSFS_PREFIX}/sys/class/thermal/thermal_zone0/temp"
|
temp_path = f"{env.SYSFS_PREFIX}/sys/class/thermal/thermal_zone0/temp"
|
||||||
@ -155,21 +155,9 @@ class HwInfoSubmanager(BaseInfoSubmanager):
|
|||||||
|
|
||||||
async def __get_cpu_percent(self) -> (float | None):
|
async def __get_cpu_percent(self) -> (float | None):
|
||||||
try:
|
try:
|
||||||
st = psutil.cpu_times_percent()
|
return int(psutil.cpu_percent(interval=1))
|
||||||
user = st.user - st.guest
|
|
||||||
nice = st.nice - st.guest_nice
|
|
||||||
idle_all = st.idle + st.iowait
|
|
||||||
system_all = st.system + st.irq + st.softirq
|
|
||||||
virtual = st.guest + st.guest_nice
|
|
||||||
total = max(1, user + nice + system_all + idle_all + st.steal + virtual)
|
|
||||||
return int(
|
|
||||||
st.nice / total * 100
|
|
||||||
+ st.user / total * 100
|
|
||||||
+ system_all / total * 100
|
|
||||||
+ (st.steal + st.guest) / total * 100
|
|
||||||
)
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
#get_logger(0).error("Can't get CPU percent: %s", ex)
|
get_logger(0).error("Can't get CPU percent: %s", ex)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def __get_mem(self) -> dict:
|
async def __get_mem(self) -> dict:
|
||||||
|
|||||||
@ -418,7 +418,7 @@ class Streamer: # pylint: disable=too-many-instance-attributes
|
|||||||
await self.__start_streamer_proc()
|
await self.__start_streamer_proc()
|
||||||
assert self.__streamer_proc is not None
|
assert self.__streamer_proc is not None
|
||||||
await aioproc.log_stdout_infinite(self.__streamer_proc, logger)
|
await aioproc.log_stdout_infinite(self.__streamer_proc, logger)
|
||||||
raise RuntimeError("Streamer unexpectedly died")
|
logger.exception("Streamer unexpectedly died")
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
# Use override.yaml to modify required settings.
|
# Use override.yaml to modify required settings.
|
||||||
# You can find a working configuration in /usr/share/kvmd/configs.default/kvmd.
|
# You can find a working configuration in /usr/share/kvmd/configs.default/kvmd.
|
||||||
|
|
||||||
override: !include [override.d, override.yaml]
|
override: !include [override.yaml]
|
||||||
|
|
||||||
logging: !include logging.yaml
|
logging: !include logging.yaml
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
# will be displayed in the web interface.
|
# will be displayed in the web interface.
|
||||||
|
|
||||||
server:
|
server:
|
||||||
host: docker
|
host: windows
|
||||||
|
|
||||||
kvm: {
|
kvm: {
|
||||||
base_on: PiKVM,
|
base_on: PiKVM,
|
||||||
|
|||||||
@ -40,12 +40,11 @@ kvmd:
|
|||||||
keymap: kvmd_data/usr/share/kvmd/keymaps/en-us
|
keymap: kvmd_data/usr/share/kvmd/keymaps/en-us
|
||||||
|
|
||||||
msd:
|
msd:
|
||||||
#type: otg
|
type: disabled
|
||||||
remount_cmd: /bin/true
|
|
||||||
msd_path: /var/lib/kvmd/msd
|
|
||||||
normalfiles_path: NormalFiles
|
|
||||||
normalfiles_size: 256
|
|
||||||
|
|
||||||
|
log_reader:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
ocr:
|
ocr:
|
||||||
langs:
|
langs:
|
||||||
- eng
|
- eng
|
||||||
@ -53,7 +52,7 @@ kvmd:
|
|||||||
|
|
||||||
streamer:
|
streamer:
|
||||||
resolution:
|
resolution:
|
||||||
default: 1280x720
|
default: 1920x1080
|
||||||
|
|
||||||
forever: true
|
forever: true
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@ -1,3 +1,3 @@
|
|||||||
PIKVM_MODEL=docker_model
|
PIKVM_MODEL=windows_model
|
||||||
PIKVM_VIDEO=docker_video
|
PIKVM_VIDEO=windows_video
|
||||||
PIKVM_BOARD=docker_board
|
PIKVM_BOARD=windows_board
|
||||||
|
|||||||
@ -899,7 +899,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="stream-info"></div>
|
<div id="stream-info"></div>
|
||||||
<button class="window-button-exit-full-tab">▼</button>
|
<button class="window-button-exit-full-tab">▼</button>
|
||||||
<div class="stream-box-online" id="stream-box"><img id="stream-image" src="/share/png/blank-stream.png">
|
<div class="stream-box-offline" id="stream-box"><img id="stream-image" src="/share/png/blank-stream.png">
|
||||||
<video class="hidden" id="stream-video" disablePictureInPicture="true" autoplay playsinline muted></video>
|
<video class="hidden" id="stream-video" disablePictureInPicture="true" autoplay playsinline muted></video>
|
||||||
<div id="stream-fullscreen-active"></div>
|
<div id="stream-fullscreen-active"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -13,7 +13,7 @@ div(id="stream-window" class="window window-resizable")
|
|||||||
div(id="stream-info")
|
div(id="stream-info")
|
||||||
|
|
||||||
button(class="window-button-exit-full-tab") ▼
|
button(class="window-button-exit-full-tab") ▼
|
||||||
div(id="stream-box" class="stream-box-online")
|
div(id="stream-box" class="stream-box-offline")
|
||||||
img(id="stream-image" src=`${png_dir}/blank-stream.png`)
|
img(id="stream-image" src=`${png_dir}/blank-stream.png`)
|
||||||
video(id="stream-video" class="hidden" disablePictureInPicture="true" autoplay playsinline muted)
|
video(id="stream-video" class="hidden" disablePictureInPicture="true" autoplay playsinline muted)
|
||||||
div(id="stream-fullscreen-active")
|
div(id="stream-fullscreen-active")
|
||||||
|
|||||||
@ -127,8 +127,9 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) {
|
|||||||
|
|
||||||
var __checkStream = function() {
|
var __checkStream = function() {
|
||||||
__findId();
|
__findId();
|
||||||
|
console.log("__state.stream.clients_stat",__state.stream.clients_stat)
|
||||||
if (__id.legnth > 0 && __id in __state.stream.clients_stat) {
|
console.log("__id",__id)
|
||||||
|
if (__id.length > 0 && __id in __state.stream.clients_stat) {
|
||||||
__setStreamActive();
|
__setStreamActive();
|
||||||
__stopChecking();
|
__stopChecking();
|
||||||
|
|
||||||
|
|||||||
317
quick_start.sh
317
quick_start.sh
@ -1,317 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
#Install Latest Stable One-KVM Dcoker Release
|
|
||||||
|
|
||||||
DOCKER_IMAGE_PATH="registry.cn-hangzhou.aliyuncs.com/silentwind/kvmd"
|
|
||||||
DOCKER_PORT="-p 8080:8080 -p 4430:4430 -p 5900:5900 -p 623:623"
|
|
||||||
DOCKER_NAME="kvmd"
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[0;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
function check_os_architecture(){
|
|
||||||
osCheck=$(uname -a)
|
|
||||||
if [[ $osCheck =~ 'x86_64' ]];then
|
|
||||||
architecture="amd64"
|
|
||||||
elif [[ $osCheck =~ 'arm64' ]] || [[ $osCheck =~ 'aarch64' ]];then
|
|
||||||
architecture="arm64"
|
|
||||||
elif [[ $osCheck =~ 'armv7l' ]];then
|
|
||||||
architecture="armv7l"
|
|
||||||
else
|
|
||||||
echo "暂不支持的系统架构,请参阅官方文档,选择受支持的系统。\n退出程序"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function check_docker_exists() {
|
|
||||||
if command -v docker &> /dev/null; then
|
|
||||||
echo "$(docker -v)"
|
|
||||||
else
|
|
||||||
echo "Docker 未安装,退出程序"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function check_sudo_exists() {
|
|
||||||
if command -v sudo > /dev/null 2>&1; then
|
|
||||||
sudo_command="sudo"
|
|
||||||
else
|
|
||||||
sudo_command=""
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function delete_kvmd_container(){
|
|
||||||
if docker ps -a --format '{{.Names}}' | grep -q '^kvmd$'; then
|
|
||||||
$sudo_command docker stop $DOCKER_NAME
|
|
||||||
$sudo_command docker rm $DOCKER_NAME
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function check_otg_device(){
|
|
||||||
$sudo_command modprobe libcomposite > /dev/null|| echo -e "${YELLOW}libcomposite 内核模块加载失败${NC}"
|
|
||||||
if [[ "$architecture" != "amd64" ]] && [[ -d "/sys/class/udc" ]]; then
|
|
||||||
if [[ "$(ls -A /sys/class/udc)" ]] || [[ "$(ls -A /sys/class/usb_role)" ]]; then
|
|
||||||
otg_devices=$(ls -A /sys/class/udc)
|
|
||||||
otg_status=$(cat /sys/class/usb_role/*/role 2>/dev/null | head -n 1)
|
|
||||||
echo -e "${GREEN}当前系统支持 OTG:$otg_devices OTG 状态:$otg_status${NC}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo -e "${RED}当前系统不支持 OTG,退出程序${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -d "/sys/kernel/config" ]];then
|
|
||||||
echo -e "${RED}当前系统不支持 configfs 文件系统,退出程序${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function check_video_device(){
|
|
||||||
if ls /dev/video* 1> /dev/null 2>&1; then
|
|
||||||
video_devices=($(ls /dev/video* 2>/dev/null))
|
|
||||||
video_num_devices=${#video_devices[@]}
|
|
||||||
echo -e ""${GREEN}找到视频设备:$(ls -A /dev/video*)${NC}""
|
|
||||||
else
|
|
||||||
echo -e "${RED}未找到任何视频采集设备,退出程序${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function check_repeat_install(){
|
|
||||||
if docker ps -a --format '{{.Names}}' | grep -q '^kvmd$'; then
|
|
||||||
echo -e "${YELLOW}检查到 kvmd 容器已存在,是否删除容器重新部署?${NC}"
|
|
||||||
read -p "y/n: " delete_choice
|
|
||||||
case $delete_choice in
|
|
||||||
y|Y)
|
|
||||||
delete_kvmd_container
|
|
||||||
;;
|
|
||||||
n|N)
|
|
||||||
echo -e "${RED}退出程序${NC}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo -e "${RED}无效的选择,请输入 y 或者 n,退出程序${NC}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
if [[ -d "kvmd_config" ]]; then
|
|
||||||
echo -e "${YELLOW}检查到此前配置文件夹已存在,是否删除此前配置文件夹?${NC}"
|
|
||||||
read -p "y/n: " delete_choice
|
|
||||||
case $delete_choice in
|
|
||||||
y|Y)
|
|
||||||
$sudo_command rm -r kvmd_config
|
|
||||||
;;
|
|
||||||
n|N)
|
|
||||||
echo -e ""
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo -e "${RED}无效的选择,请输入 y 或者 n,退出程序${NC}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function show_main_menu() {
|
|
||||||
echo -e "${BLUE}==============================${NC}"
|
|
||||||
echo -e "${BLUE} One-KVM Docker 版管理 ${NC}"
|
|
||||||
echo -e "${BLUE}==============================${NC}"
|
|
||||||
|
|
||||||
echo " 1. 安装 One-KVM Docker 版"
|
|
||||||
echo ""
|
|
||||||
echo " 2. 卸载 One-KVM Docker 版"
|
|
||||||
echo ""
|
|
||||||
echo " 3. 拉取 One-KVM 最新镜像"
|
|
||||||
echo ""
|
|
||||||
echo " 4. 更多信息"
|
|
||||||
|
|
||||||
echo -e "${BLUE}==============================${NC}"
|
|
||||||
read -p "请输入数字(1-4): " choice
|
|
||||||
while [[ "$choice" != "1" && "$choice" != "2" && "$choice" != "3" && "$choice" != "4" ]]; do
|
|
||||||
echo -e "${RED}无效的选择,请输入1-4${NC}"
|
|
||||||
read -p "请输入数字(1-4): " choice
|
|
||||||
done
|
|
||||||
case $choice in
|
|
||||||
1)
|
|
||||||
check_repeat_install
|
|
||||||
get_hid_info
|
|
||||||
get_video_info
|
|
||||||
get_audio_info
|
|
||||||
get_userinfo
|
|
||||||
get_userenv
|
|
||||||
show_install_info
|
|
||||||
get_install_command
|
|
||||||
execute_command
|
|
||||||
;;
|
|
||||||
2)
|
|
||||||
delete_kvmd_container
|
|
||||||
;;
|
|
||||||
3)
|
|
||||||
$sudo_command docker pull $DOCKER_IMAGE_PATH
|
|
||||||
;;
|
|
||||||
4)
|
|
||||||
echo -e "${BLUE}作者:${NC}\t\t默风SilentWind"
|
|
||||||
echo -e "${BLUE}文档:${NC}\t\thttps://one-kvm.mofeng.run/"
|
|
||||||
echo -e "${BLUE}Github:${NC}\thttps://github.com/mofeng-git/One-KVM"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo -e "${RED}无效的选择,请输入1-4之间的数字,退出程序${NC}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_hid_info() {
|
|
||||||
if [[ "$architecture" == "amd64" ]]; then
|
|
||||||
echo -e "${GREEN}使用的 HID 硬件类型:CH9329${NC}"
|
|
||||||
use_hid="CH9329"
|
|
||||||
else
|
|
||||||
echo -e "${GREEN}请选择使用的 HID 硬件类型:${NC}"
|
|
||||||
echo " 1. OTG"
|
|
||||||
echo " 2. CH9329"
|
|
||||||
read -p "请输入数字(1 或 2): " hardware_type
|
|
||||||
while [[ "$hardware_type" != "1" && "$hardware_type" != "2" ]]; do
|
|
||||||
echo -e "${RED}无效的选择,请输入1或2。${NC}"
|
|
||||||
read -p "请输入数字(1 或 2): " hardware_type
|
|
||||||
done
|
|
||||||
if [[ "$hardware_type" == "1" ]]; then
|
|
||||||
use_hid="OTG"
|
|
||||||
else
|
|
||||||
use_hid="CH9329"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$use_hid" == "CH9329" ]]; then
|
|
||||||
if ls /dev/ttyUSB* 1> /dev/null 2>&1; then
|
|
||||||
echo -e ""${GREEN}找到串口设备:$(ls -A /dev/ttyUSB*)${NC}""
|
|
||||||
else
|
|
||||||
echo -e "${RED}未找到任何 USB 串口设备,退出程序${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
read -p "请输入 CH9329 硬件的地址(回车使用默认值 /dev/ttyUSB0): " ch9329_address
|
|
||||||
read -p "请输入 CH9329 硬件的波特率(回车使用默认值 9600): " ch9329_serial_rate
|
|
||||||
ch9329_address=${ch9329_address:-/dev/ttyUSB0}
|
|
||||||
ch9329_serial_rate=${ch9329_serial_rate:-9600}
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$use_hid" == "OTG" ]]; then
|
|
||||||
check_otg_device
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_video_info() {
|
|
||||||
check_video_device
|
|
||||||
if [[ "$video_num_devices" == "3" ]]; then
|
|
||||||
video_default_device="/dev/video1"
|
|
||||||
echo -e "${YELLOW}经检测 /dev/video0 可能不可用,建议使用 /dev/video1${NC}"
|
|
||||||
else
|
|
||||||
video_default_device="/dev/video0"
|
|
||||||
fi
|
|
||||||
read -p "请输入视频设备路径(回车使用默认值 $video_default_device): " video_device
|
|
||||||
if [[ -z "$video_device" ]]; then
|
|
||||||
video_device=$video_default_device
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_audio_info() {
|
|
||||||
if [[ -d "/dev/snd" ]]; then
|
|
||||||
echo -e ""${GREEN}找到音频设备:$(ls -A /dev/snd)${NC}""
|
|
||||||
read -p "请输入音频设备路径(回车使用默认值 hw:0): " audio_device
|
|
||||||
if [[ -z "$audio_device" ]]; then
|
|
||||||
audio_device="hw:0"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo -e "${YELLOW}未找到任何音频采集设备${NC}"
|
|
||||||
audio_device="none"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_userinfo() {
|
|
||||||
read -p "请输入用户名(回车使用默认值 admin): " username
|
|
||||||
read -s -p "请输入密码(回车使用默认值 admin): " password
|
|
||||||
if [[ -z "$username" ]]; then
|
|
||||||
username="admin"
|
|
||||||
fi
|
|
||||||
if [[ -z "$password" ]]; then
|
|
||||||
password="admin"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_userenv() {
|
|
||||||
echo -e "\n"
|
|
||||||
read -p "额外用户环境变量(回车则留空): " userenv
|
|
||||||
}
|
|
||||||
|
|
||||||
function show_install_info() {
|
|
||||||
echo -e "\n\n${BLUE}==============================${NC}"
|
|
||||||
echo -e "${BLUE}安装信息总览:${NC}"
|
|
||||||
if [[ "$use_hid" == "CH9329" ]]; then
|
|
||||||
echo -e "CH9329 设备: \t${GREEN}$ch9329_address${NC} \tCH9329 波特率: \t${GREEN}$ch9329_serial_rate${NC}"
|
|
||||||
fi
|
|
||||||
if [[ "$use_hid" == "OTG" ]]; then
|
|
||||||
echo -e "OTG端口:\t${GREEN}$otg_devices${NC} \tOTG 状态:\t${GREEN}$otg_status${NC}"
|
|
||||||
fi
|
|
||||||
echo -e "视频设备: \t${GREEN}$video_device${NC} \t音频设备: \t${GREEN}$audio_device${NC}"
|
|
||||||
echo -e "用户名: \t${GREEN}$username${NC} \t\t密码: \t${GREEN}$password${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_install_command(){
|
|
||||||
local docker_init_command="docker run -itd --name $DOCKER_NAME"
|
|
||||||
local append_command=""
|
|
||||||
local append_env=""
|
|
||||||
|
|
||||||
if [[ "$use_hid" == "CH9329" ]]; then
|
|
||||||
append_command="--device $video_device:/dev/video0 --device $ch9329_address:/dev/ttyUSB0 -v ./kvmd_config:/etc/kvmd"
|
|
||||||
|
|
||||||
if [[ -d "/dev/snd" ]]; then
|
|
||||||
append_command="$append_command --device /dev/snd:/dev/snd -e AUDIONUM=${audio_device:3}"
|
|
||||||
fi
|
|
||||||
append_env="-e USERNAME=$username -e PASSWORD=$password -e CH9329SPEED=$ch9329_serial_rate"
|
|
||||||
docker_command="$sudo_command $docker_init_command $append_command $DOCKER_PORT $append_env $userenv $DOCKER_IMAGE_PATH"
|
|
||||||
else
|
|
||||||
append_command="--privileged=true -v /lib/modules:/lib/modules:ro -v /dev:/dev -v /sys/kernel/config:/sys/kernel/config -v ./kvmd_config:/etc/kvmd"
|
|
||||||
if [[ -d "/dev/snd" ]]; then
|
|
||||||
append_command="$append_command -e AUDIONUM=${audio_device:3}"
|
|
||||||
fi
|
|
||||||
append_env="-e OTG=1 -e USERNAME=$username -e PASSWORD=$password -e VIDEONUM=${video_device:10} -e AUDIONUM=${audio_device:3}"
|
|
||||||
docker_command="$sudo_command $docker_init_command $append_command $DOCKER_PORT $append_env $userenv $DOCKER_IMAGE_PATH"
|
|
||||||
fi
|
|
||||||
echo -e "\n${BLUE}Docker 部署命令:${NC}\n$docker_command"
|
|
||||||
echo -e "${BLUE}==============================${NC}\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
function execute_command(){
|
|
||||||
echo -e "${BLUE}One-KVM 部署中......${NC}"
|
|
||||||
eval "$docker_command"
|
|
||||||
local exit_status=$?
|
|
||||||
if [[ $exit_status -eq 0 ]]; then
|
|
||||||
echo -e "${BLUE}One-KVM 部署成功${NC}"
|
|
||||||
$sudo_command docker update --restart=always $DOCKER_NAME
|
|
||||||
if [[ "$use_hid" == "OTG" ]]; then
|
|
||||||
execute_otg_command
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo -e "${RED}One-KVM 部署失败,退出状态码为 $exit_status${NC}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function execute_otg_command(){
|
|
||||||
$sudo_command echo "device" > /sys/class/usb_role/**/role || echo -e "${YELLOW}OTG 端口切换 device 模式失败${NC}"
|
|
||||||
if grep -q "usb_role" /etc/rc.local; then
|
|
||||||
echo -e ""
|
|
||||||
else
|
|
||||||
$sudo_command sed -i '/^exit 0/i echo device > \/sys\/class\/usb_role\/\*\*\/role' /etc/rc.local
|
|
||||||
$sudo_command chmod +x /etc/rc.local
|
|
||||||
fi
|
|
||||||
if grep -q "libcomposite" /etc/modules.conf; then
|
|
||||||
echo -e ""
|
|
||||||
else
|
|
||||||
$sudo_command echo "libcomposite" >> /etc/modules.conf
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
check_os_architecture
|
|
||||||
check_docker_exists
|
|
||||||
check_sudo_exists
|
|
||||||
show_main_menu
|
|
||||||
29
tools/test_video.py
Normal file
29
tools/test_video.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# 模块导入
|
||||||
|
import numpy as np
|
||||||
|
import cv2 as cv
|
||||||
|
# 相机捕获
|
||||||
|
cap = cv.VideoCapture(0,cv.CAP_DSHOW)
|
||||||
|
#更改默认参数
|
||||||
|
cap.set(6,cv.VideoWriter.fourcc('M','J','P','G'))# 视频流格式
|
||||||
|
cap.set(5, 30);# 帧率
|
||||||
|
cap.set(3, 1280)# 帧宽
|
||||||
|
cap.set(4, 720)# 帧高
|
||||||
|
# 获取相机宽高以及帧率
|
||||||
|
width = cap.get(3)
|
||||||
|
height = cap.get(4)
|
||||||
|
frame = cap.get(5) #帧率只对视频有效,因此返回值为0
|
||||||
|
#打印信息
|
||||||
|
print(width ,height)
|
||||||
|
# 循环
|
||||||
|
while(True):
|
||||||
|
# 获取一帧图片
|
||||||
|
ret, img = cap.read()
|
||||||
|
# 显示图片
|
||||||
|
cv.imshow('img', img)
|
||||||
|
# 等待键盘事件
|
||||||
|
k = cv.waitKey(1) & 0xFF
|
||||||
|
if k == 27:
|
||||||
|
break
|
||||||
|
#资源释放
|
||||||
|
cap.release()
|
||||||
|
cv.destroyAllWindows()
|
||||||
@ -3,9 +3,9 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from typing import List, Optional, Tuple, Union, Dict, Any
|
from typing import List, Optional, Tuple, Dict
|
||||||
|
import uuid
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
import cv2
|
import cv2
|
||||||
import logging
|
import logging
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -13,8 +13,7 @@ from aiohttp import MultipartWriter, web
|
|||||||
from aiohttp.web_runner import GracefulExit
|
from aiohttp.web_runner import GracefulExit
|
||||||
|
|
||||||
class MjpegStream:
|
class MjpegStream:
|
||||||
"""MJPEG video stream class for handling video frames and providing HTTP streaming service"""
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
@ -26,19 +25,7 @@ class MjpegStream:
|
|||||||
device_name: str = "Unknown Camera",
|
device_name: str = "Unknown Camera",
|
||||||
log_requests: bool = True
|
log_requests: bool = True
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
|
||||||
Initialize MJPEG stream
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: Stream name
|
|
||||||
size: Video size (width, height)
|
|
||||||
quality: JPEG compression quality (1-100)
|
|
||||||
fps: Target frame rate
|
|
||||||
host: Server host address
|
|
||||||
port: Server port
|
|
||||||
device_name: Camera device name
|
|
||||||
log_requests: Whether to log stream requests
|
|
||||||
"""
|
|
||||||
self.name = name.lower().replace(" ", "_")
|
self.name = name.lower().replace(" ", "_")
|
||||||
self.size = size
|
self.size = size
|
||||||
self.quality = max(1, min(quality, 100))
|
self.quality = max(1, min(quality, 100))
|
||||||
@ -48,53 +35,58 @@ class MjpegStream:
|
|||||||
self._device_name = device_name
|
self._device_name = device_name
|
||||||
self.log_requests = log_requests
|
self.log_requests = log_requests
|
||||||
|
|
||||||
# Video frame and synchronization
|
|
||||||
self._frame = np.zeros((320, 240, 1), dtype=np.uint8)
|
self._frame = np.zeros((320, 240, 1), dtype=np.uint8)
|
||||||
self._lock = asyncio.Lock()
|
self._lock = asyncio.Lock()
|
||||||
self._byte_frame_window = deque(maxlen=30)
|
|
||||||
self._bandwidth_last_modified_time = time.time()
|
|
||||||
self._is_online = True
|
self._is_online = True
|
||||||
self._last_frame_time = time.time()
|
self._last_repeat_frame_time = time.time()
|
||||||
|
self._last_fps_update_time = time.time()
|
||||||
|
self._last_frame_data = None
|
||||||
|
self.per_second_fps = 0
|
||||||
|
self.frame_counter = 0
|
||||||
|
|
||||||
|
|
||||||
# 设置日志级别为ERROR,以隐藏HTTP请求日志
|
|
||||||
if not self.log_requests:
|
if not self.log_requests:
|
||||||
logging.getLogger('aiohttp.access').setLevel(logging.ERROR)
|
logging.getLogger('aiohttp.access').setLevel(logging.ERROR)
|
||||||
|
|
||||||
# Server setup
|
|
||||||
self._app = web.Application()
|
self._app = web.Application()
|
||||||
self._app.router.add_route("GET", f"/{self.name}", self._stream_handler)
|
self._app.router.add_route("GET", f"/{self.name}", self._stream_handler)
|
||||||
self._app.router.add_route("GET", "/state", self._state_handler)
|
self._app.router.add_route("GET", "/state", self._state_handler)
|
||||||
self._app.router.add_route("GET", "/", self._index_handler)
|
self._app.router.add_route("GET", "/", self._index_handler)
|
||||||
|
self._app.router.add_route("GET", "/snapshot", self._snapshot_handler)
|
||||||
self._app.is_running = False
|
self._app.is_running = False
|
||||||
|
self._clients: Dict[str, Dict] = {}
|
||||||
|
self._clients_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
|
||||||
def set_frame(self, frame: np.ndarray) -> None:
|
def set_frame(self, frame: np.ndarray) -> None:
|
||||||
"""Set the current video frame"""
|
|
||||||
self._frame = frame
|
self._frame = frame
|
||||||
self._last_frame_time = time.time()
|
|
||||||
self._is_online = True
|
self._is_online = True
|
||||||
|
|
||||||
def get_bandwidth(self) -> float:
|
|
||||||
"""Get current bandwidth usage (bytes/second)"""
|
|
||||||
if time.time() - self._bandwidth_last_modified_time >= 1:
|
|
||||||
self._byte_frame_window.clear()
|
|
||||||
return sum(self._byte_frame_window)
|
|
||||||
|
|
||||||
async def _process_frame(self) -> Tuple[np.ndarray, Dict[str, str]]:
|
async def _process_frame(self) -> Tuple[np.ndarray, Dict[str, str]]:
|
||||||
"""Process video frame (resize and JPEG encode)"""
|
|
||||||
frame = cv2.resize(
|
frame = cv2.resize(
|
||||||
self._frame, self.size or (self._frame.shape[1], self._frame.shape[0])
|
self._frame, self.size or (self._frame.shape[1], self._frame.shape[0])
|
||||||
)
|
)
|
||||||
success, encoded = cv2.imencode(
|
success, encoded = cv2.imencode(
|
||||||
".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, self.quality]
|
".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, self.quality]
|
||||||
)
|
)
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
raise ValueError("Error encoding frame")
|
raise ValueError("Error encoding frame")
|
||||||
|
|
||||||
self._byte_frame_window.append(len(encoded.tobytes()))
|
current_frame_data = encoded.tobytes()
|
||||||
self._bandwidth_last_modified_time = time.time()
|
current_time = time.time()
|
||||||
|
|
||||||
|
if current_frame_data == self._last_frame_data and current_time - self._last_repeat_frame_time < 1:
|
||||||
|
return None, {}
|
||||||
|
else:
|
||||||
|
self._last_frame_data = current_frame_data
|
||||||
|
self._last_repeat_frame_time = current_time
|
||||||
|
|
||||||
# Add KVMD-compatible header information
|
if current_time - self._last_fps_update_time >= 1:
|
||||||
|
self.per_second_fps = self.frame_counter
|
||||||
|
self.frame_counter = 0
|
||||||
|
self._last_fps_update_time = current_time
|
||||||
|
|
||||||
|
self.frame_counter += 1
|
||||||
headers = {
|
headers = {
|
||||||
"X-UStreamer-Online": str(self._is_online).lower(),
|
"X-UStreamer-Online": str(self._is_online).lower(),
|
||||||
"X-UStreamer-Width": str(frame.shape[1]),
|
"X-UStreamer-Width": str(frame.shape[1]),
|
||||||
@ -109,61 +101,69 @@ class MjpegStream:
|
|||||||
return encoded, headers
|
return encoded, headers
|
||||||
|
|
||||||
async def _stream_handler(self, request: web.Request) -> web.StreamResponse:
|
async def _stream_handler(self, request: web.Request) -> web.StreamResponse:
|
||||||
"""Handle MJPEG stream requests"""
|
client_id = request.query.get("client_id", uuid.uuid4().hex[:8])
|
||||||
|
client_key = request.query.get("key", "0")
|
||||||
|
advance_headers = request.query.get("advance_headers", "0") == "1"
|
||||||
|
|
||||||
response = web.StreamResponse(
|
response = web.StreamResponse(
|
||||||
status=200,
|
status=200,
|
||||||
reason="OK",
|
reason="OK",
|
||||||
headers={"Content-Type": "multipart/x-mixed-replace;boundary=frame"}
|
headers={
|
||||||
|
"Content-Type": "multipart/x-mixed-replace;boundary=frame",
|
||||||
|
"Set-Cookie": f"stream_client={client_key}/{client_id}; Path=/; Max-Age=30"
|
||||||
|
}
|
||||||
)
|
)
|
||||||
await response.prepare(request)
|
await response.prepare(request)
|
||||||
|
|
||||||
if self.log_requests:
|
async with self._clients_lock:
|
||||||
print(f"Stream request received: {request.path}")
|
if client_id not in self._clients:
|
||||||
|
self._clients[client_id] = {
|
||||||
|
"key": client_key,
|
||||||
|
"advance_headers": advance_headers,
|
||||||
|
"extra_headers": False,
|
||||||
|
"zero_data": False,
|
||||||
|
"fps": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
async with self._lock:
|
||||||
|
frame, headers = await self._process_frame()
|
||||||
|
if frame is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
#Enable workaround for the Chromium/Blink bug https://issues.chromium.org/issues/41199053
|
||||||
|
if advance_headers:
|
||||||
|
headers.pop('Content-Length', None)
|
||||||
|
for k in list(headers.keys()):
|
||||||
|
if k.startswith('X-UStreamer-'):
|
||||||
|
del headers[k]
|
||||||
|
|
||||||
|
with MultipartWriter("image/jpeg", boundary="frame") as mpwriter:
|
||||||
|
part = mpwriter.append(frame.tobytes(), {"Content-Type": "image/jpeg"})
|
||||||
|
for key, value in headers.items():
|
||||||
|
part.headers[key] = value
|
||||||
|
try:
|
||||||
|
await mpwriter.write(response, close_boundary=False)
|
||||||
|
except (ConnectionResetError, ConnectionAbortedError):
|
||||||
|
return web.Response(status=499)
|
||||||
|
await response.write(b"\r\n")
|
||||||
|
self._clients[client_id]["fps"]=self.per_second_fps
|
||||||
|
finally:
|
||||||
|
async with self._clients_lock:
|
||||||
|
if client_id in self._clients:
|
||||||
|
del self._clients[client_id]
|
||||||
|
|
||||||
while True:
|
|
||||||
await asyncio.sleep(1 / self.fps)
|
|
||||||
|
|
||||||
# Check if the device is online
|
|
||||||
if time.time() - self._last_frame_time > 5:
|
|
||||||
self._is_online = False
|
|
||||||
|
|
||||||
async with self._lock:
|
|
||||||
frame, headers = await self._process_frame()
|
|
||||||
|
|
||||||
with MultipartWriter("image/jpeg", boundary="frame") as mpwriter:
|
|
||||||
part = mpwriter.append(frame.tobytes(), {"Content-Type": "image/jpeg"})
|
|
||||||
for key, value in headers.items():
|
|
||||||
part.headers[key] = value
|
|
||||||
try:
|
|
||||||
await mpwriter.write(response, close_boundary=False)
|
|
||||||
except (ConnectionResetError, ConnectionAbortedError):
|
|
||||||
return web.Response(status=499)
|
|
||||||
await response.write(b"\r\n")
|
|
||||||
|
|
||||||
async def _state_handler(self, request: web.Request) -> web.Response:
|
async def _state_handler(self, request: web.Request) -> web.Response:
|
||||||
"""Handle /state requests and return device status information"""
|
|
||||||
state = {
|
state = {
|
||||||
|
"ok": "true",
|
||||||
"result": {
|
"result": {
|
||||||
"instance_id": "",
|
"instance_id": "",
|
||||||
"encoder": {
|
"encoder": {
|
||||||
"type": "CPU",
|
"type": "CPU",
|
||||||
"quality": self.quality
|
"quality": self.quality
|
||||||
},
|
},
|
||||||
"h264": {
|
|
||||||
"bitrate": 4875,
|
|
||||||
"gop": 60,
|
|
||||||
"online": self._is_online,
|
|
||||||
"fps": self.fps
|
|
||||||
},
|
|
||||||
"sinks": {
|
|
||||||
"jpeg": {
|
|
||||||
"has_clients": False
|
|
||||||
},
|
|
||||||
"h264": {
|
|
||||||
"has_clients": False
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"source": {
|
"source": {
|
||||||
"resolution": {
|
"resolution": {
|
||||||
"width": self.size[0] if self.size else self._frame.shape[1],
|
"width": self.size[0] if self.size else self._frame.shape[1],
|
||||||
@ -171,21 +171,12 @@ class MjpegStream:
|
|||||||
},
|
},
|
||||||
"online": self._is_online,
|
"online": self._is_online,
|
||||||
"desired_fps": self.fps,
|
"desired_fps": self.fps,
|
||||||
"captured_fps": 0 # You can update this with actual captured fps if needed
|
"captured_fps": self.fps
|
||||||
},
|
},
|
||||||
"stream": {
|
"stream": {
|
||||||
"queued_fps": 2, # Placeholder value, update as needed
|
"queued_fps": self.fps,
|
||||||
"clients": 1, # Placeholder value, update as needed
|
"clients": len(self._clients),
|
||||||
"clients_stat": {
|
"clients_stat": self._clients
|
||||||
"70bf63a507f71e47": {
|
|
||||||
"fps": 2, # Placeholder value, update as needed
|
|
||||||
"extra_headers": False,
|
|
||||||
"advance_headers": True,
|
|
||||||
"dual_final_frames": False,
|
|
||||||
"zero_data": False,
|
|
||||||
"key": "tIR9TtuedKIzDYZa" # Placeholder key, update as needed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -195,18 +186,30 @@ class MjpegStream:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def _index_handler(self, _: web.Request) -> web.Response:
|
async def _index_handler(self, _: web.Request) -> web.Response:
|
||||||
"""Handle root path requests and display available streams"""
|
|
||||||
html = f"""
|
html = f"""
|
||||||
<h2>Available Video Streams:</h2>
|
<html>
|
||||||
<ul>
|
<head><meta charset="utf-8"><title>uStreamer-Win</title><style>body {{font-family: monospace;}}</style></head>
|
||||||
<li><a href='http://{self._host}:{self._port}/{self.name}'>/{self.name}</a></li>
|
<body>
|
||||||
<li><a href='http://{self._host}:{self._port}/state'>/state</a></li>
|
<h3>uStreamer-Win v0.01 </h3>
|
||||||
|
<ul><hr>
|
||||||
|
<li><a href='http://{self._host}:{self._port}/{self.name}'>/{self.name}</a>
|
||||||
|
<br>Get a live stream. </li><hr><br>
|
||||||
|
<li><a href='http://{self._host}:{self._port}/snapshot'>/snapshot</a>
|
||||||
|
<br>Get a current actual image from the server.</li><hr><br>
|
||||||
|
<li><a href='http://{self._host}:{self._port}/state'>/state</a>
|
||||||
|
<br>Get JSON structure with the state of the server.</li><hr><br>
|
||||||
</ul>
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
"""
|
"""
|
||||||
return web.Response(text=html, content_type="text/html")
|
return web.Response(text=html, content_type="text/html")
|
||||||
|
|
||||||
|
async def _snapshot_handler(self, request: web.Request) -> web.Response:
|
||||||
|
async with self._lock:
|
||||||
|
frame, _ = await self._process_frame()
|
||||||
|
return web.Response(body=frame.tobytes(), content_type="image/jpeg")
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
"""Start the stream server"""
|
|
||||||
if not self._app.is_running:
|
if not self._app.is_running:
|
||||||
threading.Thread(target=self._run_server, daemon=True).start()
|
threading.Thread(target=self._run_server, daemon=True).start()
|
||||||
self._app.is_running = True
|
self._app.is_running = True
|
||||||
@ -214,8 +217,8 @@ class MjpegStream:
|
|||||||
else:
|
else:
|
||||||
print("\nServer is already running\n")
|
print("\nServer is already running\n")
|
||||||
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
"""Stop the stream server"""
|
|
||||||
if self._app.is_running:
|
if self._app.is_running:
|
||||||
self._app.is_running = False
|
self._app.is_running = False
|
||||||
print("\nStopping server...\n")
|
print("\nStopping server...\n")
|
||||||
@ -223,7 +226,6 @@ class MjpegStream:
|
|||||||
print("\nServer is not running\n")
|
print("\nServer is not running\n")
|
||||||
|
|
||||||
def _run_server(self) -> None:
|
def _run_server(self) -> None:
|
||||||
"""Run the server in a new thread"""
|
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
runner = web.AppRunner(self._app)
|
runner = web.AppRunner(self._app)
|
||||||
|
|||||||
@ -41,7 +41,6 @@ def test_camera(index, logger):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def find_camera_by_name(camera_name, logger):
|
def find_camera_by_name(camera_name, logger):
|
||||||
"""Find device index by camera name"""
|
|
||||||
if platform.system() != "Windows":
|
if platform.system() != "Windows":
|
||||||
logger.warning("Finding camera by name is only supported on Windows")
|
logger.warning("Finding camera by name is only supported on Windows")
|
||||||
return None
|
return None
|
||||||
@ -57,7 +56,6 @@ def find_camera_by_name(camera_name, logger):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def get_first_available_camera(logger):
|
def get_first_available_camera(logger):
|
||||||
"""Get the first available camera"""
|
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
if test_camera(i, logger):
|
if test_camera(i, logger):
|
||||||
return i
|
return i
|
||||||
@ -75,13 +73,10 @@ def parse_arguments():
|
|||||||
parser.add_argument('--port', type=int, default=8000, help='Server port')
|
parser.add_argument('--port', type=int, default=8000, help='Server port')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Validate arguments
|
|
||||||
if args.quality < 1 or args.quality > 100:
|
if args.quality < 1 or args.quality > 100:
|
||||||
raise ValueError("Quality must be between 1 and 100.")
|
raise ValueError("Quality must be between 1 and 100.")
|
||||||
if args.fps <= 0:
|
if args.fps <= 0:
|
||||||
raise ValueError("FPS must be greater than 0.")
|
raise ValueError("FPS must be greater than 0.")
|
||||||
|
|
||||||
# Parse resolution
|
|
||||||
try:
|
try:
|
||||||
width, height = map(int, args.resolution.split('x'))
|
width, height = map(int, args.resolution.split('x'))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -95,14 +90,9 @@ def parse_arguments():
|
|||||||
def main():
|
def main():
|
||||||
logger = configure_logging()
|
logger = configure_logging()
|
||||||
args = parse_arguments()
|
args = parse_arguments()
|
||||||
|
|
||||||
# Determine which camera device to use
|
|
||||||
device_index = None
|
device_index = None
|
||||||
|
|
||||||
if args.device_name:
|
if args.device_name:
|
||||||
if platform.system() != "Windows":
|
|
||||||
logger.error("Specifying camera by name is only supported on Windows")
|
|
||||||
return
|
|
||||||
device_index = find_camera_by_name(args.device_name, logger)
|
device_index = find_camera_by_name(args.device_name, logger)
|
||||||
if device_index is None:
|
if device_index is None:
|
||||||
logger.error(f"No available camera found with a name containing '{args.device_name}'")
|
logger.error(f"No available camera found with a name containing '{args.device_name}'")
|
||||||
@ -122,23 +112,21 @@ def main():
|
|||||||
|
|
||||||
# Initialize the camera
|
# Initialize the camera
|
||||||
try:
|
try:
|
||||||
cap = cv2.VideoCapture(device_index, cv2.CAP_DSHOW if platform.system() == "Windows" else cv2.CAP_ANY)
|
cap = cv2.VideoCapture(device_index, cv2.CAP_DSHOW)
|
||||||
|
|
||||||
if not cap.isOpened():
|
if not cap.isOpened():
|
||||||
logger.error(f"Unable to open camera {device_index}")
|
logger.error(f"Unable to open camera {device_index}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Set camera parameters
|
|
||||||
cap.set(cv2.CAP_PROP_FRAME_WIDTH, args.width)
|
cap.set(cv2.CAP_PROP_FRAME_WIDTH, args.width)
|
||||||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, args.height)
|
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, args.height)
|
||||||
|
cap.set(cv2.CAP_PROP_FRAME_COUNT, args.fps)
|
||||||
# Verify camera settings
|
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M','J','P','G'))
|
||||||
actual_width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
|
actual_width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
|
||||||
actual_height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
|
actual_height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
|
||||||
if actual_width != args.width or actual_height != args.height:
|
if actual_width != args.width or actual_height != args.height:
|
||||||
logger.warning(f"Actual resolution ({actual_width}x{actual_height}) does not match requested resolution ({args.width}x{args.height})")
|
logger.warning(f"Actual resolution ({actual_width}x{actual_height}) does not match requested resolution ({args.width}x{args.height})")
|
||||||
|
|
||||||
# Test if we can read frames
|
|
||||||
ret, _ = cap.read()
|
ret, _ = cap.read()
|
||||||
if not ret:
|
if not ret:
|
||||||
logger.error("Unable to read video frames from the camera")
|
logger.error("Unable to read video frames from the camera")
|
||||||
@ -155,13 +143,13 @@ def main():
|
|||||||
try:
|
try:
|
||||||
stream = MjpegStream(
|
stream = MjpegStream(
|
||||||
name="stream",
|
name="stream",
|
||||||
size=(int(actual_width), int(actual_height)), # Use actual resolution
|
size=(int(actual_width), int(actual_height)),
|
||||||
quality=args.quality,
|
quality=args.quality,
|
||||||
fps=args.fps,
|
fps=args.fps,
|
||||||
host=args.host,
|
host=args.host,
|
||||||
port=args.port,
|
port=args.port,
|
||||||
device_name=args.device_name or f"Camera {device_index}", # Add device name
|
device_name=args.device_name or f"Camera {device_index}",
|
||||||
log_requests=False # 设置为False以隐藏HTTP请求日志
|
log_requests=False
|
||||||
)
|
)
|
||||||
stream.start()
|
stream.start()
|
||||||
logger.info(f"Video stream started: http://{args.host}:{args.port}/stream")
|
logger.info(f"Video stream started: http://{args.host}:{args.port}/stream")
|
||||||
@ -176,20 +164,11 @@ def main():
|
|||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info("User interrupt")
|
logger.info("User interrupt")
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"An error occurred: {str(e)}")
|
|
||||||
finally:
|
finally:
|
||||||
logger.info("Cleaning up resources...")
|
logger.info("Cleaning up resources...")
|
||||||
try:
|
stream.stop()
|
||||||
stream.stop()
|
cap.release()
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error stopping the video stream: {str(e)}")
|
|
||||||
try:
|
|
||||||
cap.release()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error releasing the camera: {str(e)}")
|
|
||||||
cv2.destroyAllWindows()
|
cv2.destroyAllWindows()
|
||||||
logger.info("Program has exited")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
Loading…
x
Reference in New Issue
Block a user