进一步移植:能够打包 exe 运行

This commit is contained in:
mofeng-git 2025-02-04 11:57:12 +08:00
parent 5bf2466037
commit 45b394185a
18 changed files with 222 additions and 846 deletions

6
.gitignore vendored
View File

@ -19,7 +19,7 @@
*.pyc
*.swp
/venv/
.vscode/settings.j/son
.vscode/settings.json
kvmd_config/
__pycache__/
kvmd_data/run/kvmd/*
@ -30,3 +30,7 @@ kvmd-launcher.dist
kvmd-launcher.onefile-build
ustreamer/
node_modules/
build/
*/dist/*
*/build/*
*.spec

366
Makefile
View File

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

View File

@ -1,11 +1,68 @@
import multiprocessing
import os
import sys
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():
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 = [
'kvmd',
'-c', 'kvmd_data/etc/kvmd/main.yaml',
'-c',main_config_path,
'--run'
]
kvmd_main(argv=custom_argv)
if __name__ == '__main__':
multiprocessing.freeze_support()
start()

View File

@ -53,7 +53,7 @@ class ExtrasInfoSubmanager(BaseInfoSubmanager):
await sui.open()
except Exception as ex:
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
try:
extras: dict[str, dict] = {}

View File

@ -83,9 +83,9 @@ class HwInfoSubmanager(BaseInfoSubmanager):
)
return {
"platform": {
"type": "rpi",
"type": "windows",
"base": base,
"serial": serial,
"serial": "windows1000000000",
**platform, # type: ignore
},
"health": {
@ -124,7 +124,7 @@ class HwInfoSubmanager(BaseInfoSubmanager):
self.__dt_cache[name] = (await aiotools.read_file(path)).strip(" \t\r\n\0")
except Exception as 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]
async def __read_platform_file(self) -> dict:
@ -142,8 +142,8 @@ class HwInfoSubmanager(BaseInfoSubmanager):
"board": parsed["PIKVM_BOARD"],
}
except Exception:
get_logger(0).exception("Can't read device model")
return {"model": None, "video": None, "board": None}
#get_logger(0).exception("Can't read device model")
return {"model": "V2", "video": "USB_VIDEO", "board": "Windows"}
async def __get_cpu_temp(self) -> (float | None):
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):
try:
st = psutil.cpu_times_percent()
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
)
return int(psutil.cpu_percent(interval=1))
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
async def __get_mem(self) -> dict:

View File

@ -418,7 +418,7 @@ class Streamer: # pylint: disable=too-many-instance-attributes
await self.__start_streamer_proc()
assert self.__streamer_proc is not None
await aioproc.log_stdout_infinite(self.__streamer_proc, logger)
raise RuntimeError("Streamer unexpectedly died")
logger.exception("Streamer unexpectedly died")
except asyncio.CancelledError:
break
except Exception:

View File

@ -2,7 +2,7 @@
# Use override.yaml to modify required settings.
# 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

View File

@ -4,7 +4,7 @@
# will be displayed in the web interface.
server:
host: docker
host: windows
kvm: {
base_on: PiKVM,

View File

@ -40,12 +40,11 @@ kvmd:
keymap: kvmd_data/usr/share/kvmd/keymaps/en-us
msd:
#type: otg
remount_cmd: /bin/true
msd_path: /var/lib/kvmd/msd
normalfiles_path: NormalFiles
normalfiles_size: 256
type: disabled
log_reader:
enabled: false
ocr:
langs:
- eng
@ -53,7 +52,7 @@ kvmd:
streamer:
resolution:
default: 1280x720
default: 1920x1080
forever: true

Binary file not shown.

View File

@ -1,3 +1,3 @@
PIKVM_MODEL=docker_model
PIKVM_VIDEO=docker_video
PIKVM_BOARD=docker_board
PIKVM_MODEL=windows_model
PIKVM_VIDEO=windows_video
PIKVM_BOARD=windows_board

View File

@ -899,7 +899,7 @@
</div>
<div id="stream-info"></div>
<button class="window-button-exit-full-tab">&#9660;</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>
<div id="stream-fullscreen-active"></div>
</div>

View File

@ -13,7 +13,7 @@ div(id="stream-window" class="window window-resizable")
div(id="stream-info")
button(class="window-button-exit-full-tab") &#9660;
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`)
video(id="stream-video" class="hidden" disablePictureInPicture="true" autoplay playsinline muted)
div(id="stream-fullscreen-active")

View File

@ -127,8 +127,9 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) {
var __checkStream = function() {
__findId();
if (__id.legnth > 0 && __id in __state.stream.clients_stat) {
console.log("__state.stream.clients_stat",__state.stream.clients_stat)
console.log("__id",__id)
if (__id.length > 0 && __id in __state.stream.clients_stat) {
__setStreamActive();
__stopChecking();

View File

@ -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
View 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()

View File

@ -3,9 +3,9 @@ import threading
import time
import json
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 logging
import numpy as np
@ -13,8 +13,7 @@ from aiohttp import MultipartWriter, web
from aiohttp.web_runner import GracefulExit
class MjpegStream:
"""MJPEG video stream class for handling video frames and providing HTTP streaming service"""
def __init__(
self,
name: str,
@ -26,19 +25,7 @@ class MjpegStream:
device_name: str = "Unknown Camera",
log_requests: bool = True
) -> 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.size = size
self.quality = max(1, min(quality, 100))
@ -48,53 +35,58 @@ class MjpegStream:
self._device_name = device_name
self.log_requests = log_requests
# Video frame and synchronization
self._frame = np.zeros((320, 240, 1), dtype=np.uint8)
self._lock = asyncio.Lock()
self._byte_frame_window = deque(maxlen=30)
self._bandwidth_last_modified_time = time.time()
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:
logging.getLogger('aiohttp.access').setLevel(logging.ERROR)
# Server setup
self._app = web.Application()
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", "/", self._index_handler)
self._app.router.add_route("GET", "/snapshot", self._snapshot_handler)
self._app.is_running = False
self._clients: Dict[str, Dict] = {}
self._clients_lock = asyncio.Lock()
def set_frame(self, frame: np.ndarray) -> None:
"""Set the current video frame"""
self._frame = frame
self._last_frame_time = time.time()
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]]:
"""Process video frame (resize and JPEG encode)"""
frame = cv2.resize(
self._frame, self.size or (self._frame.shape[1], self._frame.shape[0])
)
success, encoded = cv2.imencode(
".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, self.quality]
)
if not success:
raise ValueError("Error encoding frame")
self._byte_frame_window.append(len(encoded.tobytes()))
self._bandwidth_last_modified_time = time.time()
current_frame_data = encoded.tobytes()
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 = {
"X-UStreamer-Online": str(self._is_online).lower(),
"X-UStreamer-Width": str(frame.shape[1]),
@ -109,61 +101,69 @@ class MjpegStream:
return encoded, headers
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(
status=200,
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)
if self.log_requests:
print(f"Stream request received: {request.path}")
async with self._clients_lock:
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:
"""Handle /state requests and return device status information"""
state = {
"ok": "true",
"result": {
"instance_id": "",
"encoder": {
"type": "CPU",
"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": {
"resolution": {
"width": self.size[0] if self.size else self._frame.shape[1],
@ -171,21 +171,12 @@ class MjpegStream:
},
"online": self._is_online,
"desired_fps": self.fps,
"captured_fps": 0 # You can update this with actual captured fps if needed
"captured_fps": self.fps
},
"stream": {
"queued_fps": 2, # Placeholder value, update as needed
"clients": 1, # Placeholder value, update as needed
"clients_stat": {
"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
}
}
"queued_fps": self.fps,
"clients": len(self._clients),
"clients_stat": self._clients
}
}
}
@ -195,18 +186,30 @@ class MjpegStream:
)
async def _index_handler(self, _: web.Request) -> web.Response:
"""Handle root path requests and display available streams"""
html = f"""
<h2>Available Video Streams:</h2>
<ul>
<li><a href='http://{self._host}:{self._port}/{self.name}'>/{self.name}</a></li>
<li><a href='http://{self._host}:{self._port}/state'>/state</a></li>
<html>
<head><meta charset="utf-8"><title>uStreamer-Win</title><style>body {{font-family: monospace;}}</style></head>
<body>
<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>
</body>
</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:
"""Start the stream server"""
if not self._app.is_running:
threading.Thread(target=self._run_server, daemon=True).start()
self._app.is_running = True
@ -214,8 +217,8 @@ class MjpegStream:
else:
print("\nServer is already running\n")
def stop(self) -> None:
"""Stop the stream server"""
if self._app.is_running:
self._app.is_running = False
print("\nStopping server...\n")
@ -223,7 +226,6 @@ class MjpegStream:
print("\nServer is not running\n")
def _run_server(self) -> None:
"""Run the server in a new thread"""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
runner = web.AppRunner(self._app)

View File

@ -41,7 +41,6 @@ def test_camera(index, logger):
return False
def find_camera_by_name(camera_name, logger):
"""Find device index by camera name"""
if platform.system() != "Windows":
logger.warning("Finding camera by name is only supported on Windows")
return None
@ -57,7 +56,6 @@ def find_camera_by_name(camera_name, logger):
return None
def get_first_available_camera(logger):
"""Get the first available camera"""
for i in range(5):
if test_camera(i, logger):
return i
@ -75,13 +73,10 @@ def parse_arguments():
parser.add_argument('--port', type=int, default=8000, help='Server port')
args = parser.parse_args()
# Validate arguments
if args.quality < 1 or args.quality > 100:
raise ValueError("Quality must be between 1 and 100.")
if args.fps <= 0:
raise ValueError("FPS must be greater than 0.")
# Parse resolution
try:
width, height = map(int, args.resolution.split('x'))
except ValueError:
@ -95,14 +90,9 @@ def parse_arguments():
def main():
logger = configure_logging()
args = parse_arguments()
# Determine which camera device to use
device_index = None
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)
if device_index is None:
logger.error(f"No available camera found with a name containing '{args.device_name}'")
@ -122,23 +112,21 @@ def main():
# Initialize the camera
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():
logger.error(f"Unable to open camera {device_index}")
return
# Set camera parameters
cap.set(cv2.CAP_PROP_FRAME_WIDTH, args.width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, args.height)
# Verify camera settings
cap.set(cv2.CAP_PROP_FRAME_COUNT, args.fps)
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M','J','P','G'))
actual_width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
actual_height = cap.get(cv2.CAP_PROP_FRAME_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})")
# Test if we can read frames
ret, _ = cap.read()
if not ret:
logger.error("Unable to read video frames from the camera")
@ -155,13 +143,13 @@ def main():
try:
stream = MjpegStream(
name="stream",
size=(int(actual_width), int(actual_height)), # Use actual resolution
size=(int(actual_width), int(actual_height)),
quality=args.quality,
fps=args.fps,
host=args.host,
port=args.port,
device_name=args.device_name or f"Camera {device_index}", # Add device name
log_requests=False # 设置为False以隐藏HTTP请求日志
device_name=args.device_name or f"Camera {device_index}",
log_requests=False
)
stream.start()
logger.info(f"Video stream started: http://{args.host}:{args.port}/stream")
@ -176,20 +164,11 @@ def main():
except KeyboardInterrupt:
logger.info("User interrupt")
except Exception as e:
logger.error(f"An error occurred: {str(e)}")
finally:
logger.info("Cleaning up resources...")
try:
stream.stop()
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)}")
stream.stop()
cap.release()
cv2.destroyAllWindows()
logger.info("Program has exited")
if __name__ == "__main__":
main()