diff --git a/.gitignore b/.gitignore
index 744d9f68..7b477c26 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
\ No newline at end of file
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 4941bf4c..00000000
--- a/Makefile
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/kvmd-launcher.py b/kvmd-launcher.py
index 5ca4ae3f..ef44b340 100644
--- a/kvmd-launcher.py
+++ b/kvmd-launcher.py
@@ -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()
\ No newline at end of file
diff --git a/kvmd/apps/kvmd/info/extras.py b/kvmd/apps/kvmd/info/extras.py
index 6cae6574..80dbfc44 100644
--- a/kvmd/apps/kvmd/info/extras.py
+++ b/kvmd/apps/kvmd/info/extras.py
@@ -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] = {}
diff --git a/kvmd/apps/kvmd/info/hw.py b/kvmd/apps/kvmd/info/hw.py
index db91e586..bc4af604 100644
--- a/kvmd/apps/kvmd/info/hw.py
+++ b/kvmd/apps/kvmd/info/hw.py
@@ -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:
diff --git a/kvmd/apps/kvmd/streamer.py b/kvmd/apps/kvmd/streamer.py
index 0f117e4a..82a7f524 100644
--- a/kvmd/apps/kvmd/streamer.py
+++ b/kvmd/apps/kvmd/streamer.py
@@ -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:
diff --git a/kvmd_data/etc/kvmd/main.yaml b/kvmd_data/etc/kvmd/main.yaml
index 6d60dcf2..d54f79ed 100644
--- a/kvmd_data/etc/kvmd/main.yaml
+++ b/kvmd_data/etc/kvmd/main.yaml
@@ -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
diff --git a/kvmd_data/etc/kvmd/meta.yaml b/kvmd_data/etc/kvmd/meta.yaml
index f024a83f..43bf3591 100644
--- a/kvmd_data/etc/kvmd/meta.yaml
+++ b/kvmd_data/etc/kvmd/meta.yaml
@@ -4,7 +4,7 @@
# will be displayed in the web interface.
server:
- host: docker
+ host: windows
kvm: {
base_on: PiKVM,
diff --git a/kvmd_data/etc/kvmd/override.yaml b/kvmd_data/etc/kvmd/override.yaml
index 40f04548..70f81f55 100644
--- a/kvmd_data/etc/kvmd/override.yaml
+++ b/kvmd_data/etc/kvmd/override.yaml
@@ -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
diff --git a/kvmd_data/usr/bin/ustreamer b/kvmd_data/usr/bin/ustreamer
deleted file mode 100755
index 8c0a1571..00000000
Binary files a/kvmd_data/usr/bin/ustreamer and /dev/null differ
diff --git a/kvmd_data/usr/share/kvmd/platform b/kvmd_data/usr/share/kvmd/platform
index 0f00370c..0c9d0ac4 100644
--- a/kvmd_data/usr/share/kvmd/platform
+++ b/kvmd_data/usr/share/kvmd/platform
@@ -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
diff --git a/kvmd_data/usr/share/kvmd/web/kvm/index.html b/kvmd_data/usr/share/kvmd/web/kvm/index.html
index 774a64bb..f5799301 100644
--- a/kvmd_data/usr/share/kvmd/web/kvm/index.html
+++ b/kvmd_data/usr/share/kvmd/web/kvm/index.html
@@ -899,7 +899,7 @@
▼
-
+
diff --git a/kvmd_data/usr/share/kvmd/web/kvm/window-stream.pug b/kvmd_data/usr/share/kvmd/web/kvm/window-stream.pug
index ee75646c..918260ab 100644
--- a/kvmd_data/usr/share/kvmd/web/kvm/window-stream.pug
+++ b/kvmd_data/usr/share/kvmd/web/kvm/window-stream.pug
@@ -13,7 +13,7 @@ div(id="stream-window" class="window window-resizable")
div(id="stream-info")
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`)
video(id="stream-video" class="hidden" disablePictureInPicture="true" autoplay playsinline muted)
div(id="stream-fullscreen-active")
diff --git a/kvmd_data/usr/share/kvmd/web/share/js/kvm/stream_mjpeg.js b/kvmd_data/usr/share/kvmd/web/share/js/kvm/stream_mjpeg.js
index 5de3e1e7..6b8b775d 100644
--- a/kvmd_data/usr/share/kvmd/web/share/js/kvm/stream_mjpeg.js
+++ b/kvmd_data/usr/share/kvmd/web/share/js/kvm/stream_mjpeg.js
@@ -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();
diff --git a/quick_start.sh b/quick_start.sh
deleted file mode 100755
index a304ab26..00000000
--- a/quick_start.sh
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/tools/test_video.py b/tools/test_video.py
new file mode 100644
index 00000000..0e0237f3
--- /dev/null
+++ b/tools/test_video.py
@@ -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()
\ No newline at end of file
diff --git a/ustreamer-win/mjpeg_stream.py b/ustreamer-win/mjpeg_stream.py
index b9b13529..87bfbd27 100644
--- a/ustreamer-win/mjpeg_stream.py
+++ b/ustreamer-win/mjpeg_stream.py
@@ -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"""
-
Available Video Streams:
-
- /{self.name}
- /state
+
+ uStreamer-Win
+
+ uStreamer-Win v0.01
+
+ /{self.name}
+ Get a live stream.
+ /snapshot
+ Get a current actual image from the server.
+ /state
+ Get JSON structure with the state of the server.
+
+
"""
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)
diff --git a/ustreamer-win/ustreamer-win.py b/ustreamer-win/ustreamer-win.py
index 36627b79..7557824d 100644
--- a/ustreamer-win/ustreamer-win.py
+++ b/ustreamer-win/ustreamer-win.py
@@ -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()
\ No newline at end of file