From ddb4d752c0fc554756fac010c6dabe3975b2da5a Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Mon, 3 Feb 2025 12:55:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E9=80=82=E9=85=8Dwindows?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kvmd/aiogp.py | 42 +--- kvmd/aioproc.py | 10 +- kvmd/aiotools.py | 6 +- kvmd/apps/__init__.py | 9 +- kvmd/apps/kvmd/info/extras.py | 3 +- kvmd/apps/kvmd/info/hw.py | 4 +- kvmd/apps/kvmd/info/system.py | 14 +- kvmd/apps/kvmd/ocr.py | 2 +- kvmd/apps/kvmd/server.py | 10 +- kvmd/apps/kvmd/streamer.py | 6 +- kvmd/clients/__init__.py | 4 +- kvmd/clients/streamer.py | 2 +- kvmd/keyboard/printer.py | 25 -- kvmd/libc.py | 32 +-- kvmd/plugins/__init__.py | 8 +- kvmd/plugins/hid/ch9329/__init__.py | 5 + kvmd/plugins/ugpio/gpio.py | 2 +- kvmd_data/etc/kvmd/override.yaml | 56 +++-- kvmd_data/usr/share/kvmd/web/kvm/index.html | 2 +- .../usr/share/kvmd/web/kvm/window-stream.pug | 2 +- kvmd_data/win/true.exe | Bin 0 -> 128628 bytes tools/list_devices.py | 46 ++++ ustreamer-win/mjpeg_stream.py | 233 ++++++++++++++++++ ustreamer-win/ustreamer-win.py | 197 +++++++++++++++ 24 files changed, 567 insertions(+), 153 deletions(-) create mode 100644 kvmd_data/win/true.exe create mode 100644 tools/list_devices.py create mode 100644 ustreamer-win/mjpeg_stream.py create mode 100644 ustreamer-win/ustreamer-win.py diff --git a/kvmd/aiogp.py b/kvmd/aiogp.py index 3ee0ea2b..d6fd4a5f 100644 --- a/kvmd/aiogp.py +++ b/kvmd/aiogp.py @@ -24,7 +24,7 @@ import asyncio import threading import dataclasses -import gpiod +#import gpiod from . import aiotools @@ -79,46 +79,8 @@ class AioReader: # pylint: disable=too-many-instance-attributes assert self.__loop pins = sorted(self.__pins) - with gpiod.request_lines( - self.__path, - consumer=self.__consumer, - config={tuple(pins): gpiod.LineSettings(edge_detection=gpiod.line.Edge.BOTH)}, - ) as line_req: + - line_req.wait_edge_events(0.1) - self.__values = { - pin: _DebouncedValue( - initial=bool(value.value), - debounce=self.__pins[pin].debounce, - notifier=self.__notifier, - loop=self.__loop, - ) - for (pin, value) in zip(pins, line_req.get_values(pins)) - } - self.__loop.call_soon_threadsafe(self.__notifier.notify) - - while not self.__stop_event.is_set(): - if line_req.wait_edge_events(1): - new: dict[int, bool] = {} - for event in line_req.read_edge_events(): - (pin, value) = self.__parse_event(event) - new[pin] = value - for (pin, value) in new.items(): - self.__values[pin].set(value) - else: # Timeout - # XXX: Лимит был актуален для 1.6. Надо проверить, поменялось ли это в 2.x. - # Размер буфера ядра - 16 эвентов на линии. При превышении этого числа, - # новые эвенты потеряются. Это не баг, это фича, как мне объяснили в LKML. - # Штош. Будем с этим жить и синхронизировать состояния при таймауте. - for (pin, value) in zip(pins, line_req.get_values(pins)): - self.__values[pin].set(bool(value.value)) # type: ignore - - def __parse_event(self, event: gpiod.EdgeEvent) -> tuple[int, bool]: - if event.event_type == event.Type.RISING_EDGE: - return (event.line_offset, True) - elif event.event_type == event.Type.FALLING_EDGE: - return (event.line_offset, False) - raise RuntimeError(f"Invalid event {event} type: {event.type}") class _DebouncedValue: diff --git a/kvmd/aioproc.py b/kvmd/aioproc.py index 376df004..66c1d43f 100644 --- a/kvmd/aioproc.py +++ b/kvmd/aioproc.py @@ -21,6 +21,7 @@ import os +import platform import signal import asyncio import asyncio.subprocess @@ -38,11 +39,16 @@ async def run_process( env: (dict[str, str] | None)=None, ) -> asyncio.subprocess.Process: # pylint: disable=no-member + if platform.system() != 'Windows': + preexec_fn=os.setpgrp + else: + preexec_fn=None # 或者选择适合 Windows 的其他方式 + return (await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=(asyncio.subprocess.DEVNULL if err_to_null else asyncio.subprocess.STDOUT), - preexec_fn=os.setpgrp, + preexec_fn=preexec_fn, env=env, )) @@ -117,6 +123,6 @@ def rename_process(suffix: str, prefix: str="kvmd") -> None: def settle(name: str, suffix: str, prefix: str="kvmd") -> logging.Logger: logger = get_logger(1) logger.info("Started %s pid=%d", name, os.getpid()) - os.setpgrp() + #os.setpgrp() rename_process(suffix, prefix) return logger diff --git a/kvmd/aiotools.py b/kvmd/aiotools.py index a47c94c6..f2776770 100644 --- a/kvmd/aiotools.py +++ b/kvmd/aiotools.py @@ -27,6 +27,7 @@ import ssl import functools import types import typing +import platform from typing import Callable from typing import Awaitable @@ -56,8 +57,9 @@ def run(coro: Coroutine, final: (Coroutine | None)=None) -> None: raise SystemExit() loop = asyncio.get_event_loop() - loop.add_signal_handler(signal.SIGINT, sigint_handler) - loop.add_signal_handler(signal.SIGTERM, sigterm_handler) + if platform.system() != 'Windows': + loop.add_signal_handler(signal.SIGINT, sigint_handler) + loop.add_signal_handler(signal.SIGTERM, sigterm_handler) main_task = loop.create_task(coro) try: diff --git a/kvmd/apps/__init__.py b/kvmd/apps/__init__.py index d9a2d97a..280f6125 100644 --- a/kvmd/apps/__init__.py +++ b/kvmd/apps/__init__.py @@ -181,12 +181,13 @@ def _init_config(config_path: str, override_options: list[str], **load_flags: bo _patch_raw(raw_config) config = make_config(raw_config, scheme) - if _patch_dynamic(raw_config, config, scheme, **load_flags): - config = make_config(raw_config, scheme) - - return config + except (ConfigError, UnknownPluginError) as ex: raise SystemExit(f"ConfigError: {ex}") + if _patch_dynamic(raw_config, config, scheme, **load_flags): + config = make_config(raw_config, scheme) + + return config def _patch_raw(raw_config: dict) -> None: # pylint: disable=too-many-branches diff --git a/kvmd/apps/kvmd/info/extras.py b/kvmd/apps/kvmd/info/extras.py index 1e69748c..6cae6574 100644 --- a/kvmd/apps/kvmd/info/extras.py +++ b/kvmd/apps/kvmd/info/extras.py @@ -23,6 +23,7 @@ import os import re import asyncio +import sys from typing import AsyncGenerator @@ -51,7 +52,7 @@ class ExtrasInfoSubmanager(BaseInfoSubmanager): sui = sysunit.SystemdUnitInfo() await sui.open() except Exception as ex: - if not os.path.exists("/etc/kvmd/.docker_flag"): + 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)) sui = None try: diff --git a/kvmd/apps/kvmd/info/hw.py b/kvmd/apps/kvmd/info/hw.py index 3c444760..db91e586 100644 --- a/kvmd/apps/kvmd/info/hw.py +++ b/kvmd/apps/kvmd/info/hw.py @@ -169,7 +169,7 @@ class HwInfoSubmanager(BaseInfoSubmanager): + (st.steal + st.guest) / total * 100 ) 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: @@ -218,7 +218,7 @@ class HwInfoSubmanager(BaseInfoSubmanager): async def __parse_vcgencmd(self, arg: str, parser: Callable[[str], _RetvalT]) -> (_RetvalT | None): cmd = [*self.__vcgencmd_cmd, arg] try: - text = (await aioproc.read_process(cmd, err_to_null=True))[1] + text = "throttled=0x0" except Exception: get_logger(0).exception("Error while executing: %s", tools.cmdfmt(cmd)) return None diff --git a/kvmd/apps/kvmd/info/system.py b/kvmd/apps/kvmd/info/system.py index d4a450de..de21b824 100644 --- a/kvmd/apps/kvmd/info/system.py +++ b/kvmd/apps/kvmd/info/system.py @@ -76,12 +76,14 @@ class SystemInfoSubmanager(BaseInfoSubmanager): except Exception: get_logger(0).exception("Can't get streamer info") else: - try: - for line in features_text.split("\n"): - (status, name) = map(str.strip, line.split(" ")) - features[name] = (status == "+") - except Exception: - get_logger(0).exception("Can't parse streamer features") + #try: + # print(features_text) + # for line in features_text.split("\n"): + # (status, name) = map(str.strip, line.split(" ")) + # features[name] = (status == "+") + #except Exception: + # get_logger(0).exception("Can't parse streamer features") + pass return { "app": os.path.basename(path), "version": version, diff --git a/kvmd/apps/kvmd/ocr.py b/kvmd/apps/kvmd/ocr.py index 367c0c80..13710d0c 100644 --- a/kvmd/apps/kvmd/ocr.py +++ b/kvmd/apps/kvmd/ocr.py @@ -78,7 +78,7 @@ def _load_libtesseract() -> (ctypes.CDLL | None): setattr(func, "argtypes", argtypes) return lib except Exception as ex: - warnings.warn(f"Can't load libtesseract: {ex}", RuntimeWarning) + #warnings.warn(f"Can't load libtesseract: {ex}", RuntimeWarning) return None diff --git a/kvmd/apps/kvmd/server.py b/kvmd/apps/kvmd/server.py index 2efc4e1d..40427114 100644 --- a/kvmd/apps/kvmd/server.py +++ b/kvmd/apps/kvmd/server.py @@ -38,8 +38,10 @@ from aiohttp.web import StreamResponse import os from aiohttp import ClientConnectionError +from aiohttp import ClientPayloadError from aiohttp import ClientSession from aiohttp import UnixConnector +from aiohttp import ClientTimeout from urllib.parse import urlencode @@ -329,9 +331,11 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins socket_path = self.__streamer.get_path() query_string = urlencode(request.query) headers = request.headers.copy() + time_out = ClientTimeout(total=10) try: - async with ClientSession(connector=UnixConnector(path=socket_path)) as session: - backend_url = f'http://localhost/stream?{query_string}' if query_string else 'http://localhost/stream' + #async with ClientSession(connector=UnixConnector(path=socket_path)) as session: + async with ClientSession() as session: + backend_url = f'http://localhost:8000/stream?{query_string}' if query_string else 'http://localhost:8000/stream' async with session.get(backend_url, headers=headers) as resp: response = StreamResponse(status=resp.status, reason=resp.reason, headers=resp.headers) await response.prepare(request) @@ -341,7 +345,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins break await response.write(chunk) return response - except ClientConnectionError: + except (ClientConnectionError, ClientPayloadError, ConnectionResetError): return Response(status=500, text="Client connection was closed") diff --git a/kvmd/apps/kvmd/streamer.py b/kvmd/apps/kvmd/streamer.py index 3422a9bf..0f117e4a 100644 --- a/kvmd/apps/kvmd/streamer.py +++ b/kvmd/apps/kvmd/streamer.py @@ -20,6 +20,7 @@ # ========================================================================== # +import platform import signal import asyncio import asyncio.subprocess @@ -302,7 +303,8 @@ class Streamer: # pylint: disable=too-many-instance-attributes self.__notifier.notify(self.__ST_STREAMER) get_logger(0).info("Installing SIGUSR2 streamer handler ...") - asyncio.get_event_loop().add_signal_handler(signal.SIGUSR2, signal_handler) + if platform.system() != 'Windows': + asyncio.get_event_loop().add_signal_handler(signal.SIGUSR2, signal_handler) prev: dict = {} while True: @@ -338,7 +340,7 @@ class Streamer: # pylint: disable=too-many-instance-attributes session = self.__ensure_client_session() try: return (await session.get_state()) - except (aiohttp.ClientConnectionError, aiohttp.ServerConnectionError): + except (aiohttp.ClientConnectionError, aiohttp.ServerConnectionError,TimeoutError,asyncio.CancelledError,asyncio.TimeoutError,asyncio.CancelledError): pass except Exception: get_logger().exception("Invalid streamer response from /state") diff --git a/kvmd/clients/__init__.py b/kvmd/clients/__init__.py index f0645fd2..e06bda14 100644 --- a/kvmd/clients/__init__.py +++ b/kvmd/clients/__init__.py @@ -75,12 +75,12 @@ class BaseHttpClient: def _make_http_session(self, headers: dict[str, str] | None = None) -> aiohttp.ClientSession: connector = None #这里临时使用 socket ,后期考虑是否使用 http 方式 - use_unix_socket = True + use_unix_socket = False if use_unix_socket: connector = aiohttp.UnixConnector(path=self.__unix_path) base_url = "http://localhost:0" # 继续使用 Unix 域套接字 else: - base_url = "http://127.0.0.1:8001" # 使用指定的 IP 和端口 + base_url = "http://127.0.0.1:8000" # 使用指定的 IP 和端口 #print("base_url:", base_url) return aiohttp.ClientSession( diff --git a/kvmd/clients/streamer.py b/kvmd/clients/streamer.py index 5369892e..5ea6af98 100644 --- a/kvmd/clients/streamer.py +++ b/kvmd/clients/streamer.py @@ -32,7 +32,7 @@ from typing import Generator from typing import AsyncGenerator import aiohttp -import ustreamer +#import ustreamer from PIL import Image as PilImage diff --git a/kvmd/keyboard/printer.py b/kvmd/keyboard/printer.py index efee6d44..07e75e75 100644 --- a/kvmd/keyboard/printer.py +++ b/kvmd/keyboard/printer.py @@ -30,29 +30,8 @@ from .mappings import WebModifiers # ===== -def _load_libxkbcommon() -> ctypes.CDLL: - path = ctypes.util.find_library("xkbcommon") - if not path: - raise RuntimeError("Where is libxkbcommon?") - assert path - lib = ctypes.CDLL(path) - for (name, restype, argtypes) in [ - ("xkb_utf32_to_keysym", ctypes.c_uint32, [ctypes.c_uint32]), - ]: - func = getattr(lib, name) - if not func: - raise RuntimeError(f"Where is libc.{name}?") - setattr(func, "restype", restype) - setattr(func, "argtypes", argtypes) - return lib -_libxkbcommon = _load_libxkbcommon() - - -def _ch_to_keysym(ch: str) -> int: - assert len(ch) == 1 - return _libxkbcommon.xkb_utf32_to_keysym(ord(ch)) # ===== @@ -84,10 +63,6 @@ def text_to_web_keys( # pylint: disable=too-many-branches ch = "--" if not ch.isprintable(): continue - try: - keys = symmap[_ch_to_keysym(ch)] - except Exception: - continue for (modifiers, key) in keys.items(): if modifiers & SymmapModifiers.CTRL: diff --git a/kvmd/libc.py b/kvmd/libc.py index 53b25733..f004acde 100644 --- a/kvmd/libc.py +++ b/kvmd/libc.py @@ -34,34 +34,4 @@ from ctypes import c_void_p # ===== def _load_libc() -> ctypes.CDLL: - path = ctypes.util.find_library("c") - if not path: - raise RuntimeError("Where is libc?") - assert path - lib = ctypes.CDLL(path) - for (name, restype, argtypes) in [ - ("inotify_init", c_int, []), - ("inotify_add_watch", c_int, [c_int, c_char_p, c_uint32]), - ("inotify_rm_watch", c_int, [c_int, c_uint32]), - ("renameat2", c_int, [c_int, c_char_p, c_int, c_char_p, c_uint]), - ("free", c_int, [c_void_p]), - ]: - func = getattr(lib, name) - if not func: - raise RuntimeError(f"Where is libc.{name}?") - setattr(func, "restype", restype) - setattr(func, "argtypes", argtypes) - return lib - - -_libc = _load_libc() - - -# ===== -get_errno = ctypes.get_errno - -inotify_init = _libc.inotify_init -inotify_add_watch = _libc.inotify_add_watch -inotify_rm_watch = _libc.inotify_rm_watch -renameat2 = _libc.renameat2 -free = _libc.free + pass diff --git a/kvmd/plugins/__init__.py b/kvmd/plugins/__init__.py index ce2567f1..17bd4622 100644 --- a/kvmd/plugins/__init__.py +++ b/kvmd/plugins/__init__.py @@ -52,8 +52,8 @@ def get_plugin_class(sub: str, name: str) -> type[BasePlugin]: assert name if name.startswith("_"): raise UnknownPluginError(f"Unknown plugin '{sub}/{name}'") - try: - module = importlib.import_module(f"kvmd.plugins.{sub}.{name}") - except ModuleNotFoundError: - raise UnknownPluginError(f"Unknown plugin '{sub}/{name}'") + #try: + module = importlib.import_module(f"kvmd.plugins.{sub}.{name}") + #except ModuleNotFoundError: + #raise UnknownPluginError(f"Unknown plugin '{sub}/{name}'") return getattr(module, "Plugin") diff --git a/kvmd/plugins/hid/ch9329/__init__.py b/kvmd/plugins/hid/ch9329/__init__.py index 1b235090..05459fa2 100644 --- a/kvmd/plugins/hid/ch9329/__init__.py +++ b/kvmd/plugins/hid/ch9329/__init__.py @@ -200,6 +200,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst while not self.__stop_event.is_set(): try: self.__hid_loop() + time.sleep(1) except Exception: logger.exception("Unexpected error in the run loop") time.sleep(1) @@ -222,6 +223,10 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst self.__process_cmd(conn, b"") else: self.__process_cmd(conn, cmd) + except KeyboardInterrupt: + get_logger(0).info("KeyboardInterrupt received, exiting HID loop.") + self.clear_events() + break except Exception: self.clear_events() get_logger(0).exception("Unexpected error in the HID loop") diff --git a/kvmd/plugins/ugpio/gpio.py b/kvmd/plugins/ugpio/gpio.py index 6cda826b..311acaf1 100644 --- a/kvmd/plugins/ugpio/gpio.py +++ b/kvmd/plugins/ugpio/gpio.py @@ -23,7 +23,7 @@ from typing import Callable from typing import Any -import gpiod +#import gpiod from ... import aiotools from ... import aiogp diff --git a/kvmd_data/etc/kvmd/override.yaml b/kvmd_data/etc/kvmd/override.yaml index 4ee138dd..40f04548 100644 --- a/kvmd_data/etc/kvmd/override.yaml +++ b/kvmd_data/etc/kvmd/override.yaml @@ -26,7 +26,7 @@ kvmd: hid: type: ch9329 - device: /dev/ttyUSB0 + device: COM7 speed: 115200 read_timeout: 0.3 @@ -53,7 +53,7 @@ kvmd: streamer: resolution: - default: 1920x1080 + default: 1280x720 forever: true @@ -64,32 +64,30 @@ kvmd: h264_bitrate: default: 8000 - cmd: - - "kvmd_data/usr/bin/ustreamer" - - "--device=/dev/video0" - - "--persistent" - - "--format=mjpeg" - - "--resolution={resolution}" - - "--desired-fps={desired_fps}" - - "--drop-same-frames=30" - - "--last-as-blank=0" - - "--unix={unix}" - - "--unix-rm" - - "--unix-mode=777" - - "--exit-on-parent-death" - - "--notify-parent" - - "--no-log-colors" - - "--jpeg-sink=kvmd::ustreamer::jpeg" - - "--jpeg-sink-mode=0660" - - "--slowdown" + pre_start_cmd: kvmd_data/win/true.exe + post_stop_cmd: kvmd_data/win/true.exe - unix: kvmd_data/run/kvmd/ustreamer.sock + cmd: + - "C:/Users/mofen/miniconda3/python.exe" + - "ustreamer-win/ustreamer-win.py" + - "--device=0" + - "--resolution={resolution}" + - "--fps={desired_fps}" + - "--quality=100" + + + unix: http://localhost:8000 ipmi: auth: file: kvmd_data/etc/kvmd/ipmipasswd +pst: + remount_cmd: + - "kvmd_data/win/true.exe" + + vnc: keymap: kvmd_data/usr/share/kvmd/keymaps/en-us mouse_output: usb @@ -110,10 +108,20 @@ vnc: otgnet: commands: + pre_start_cmd: + - "kvmd_data/win/true.exe" + post_stop_cmd: + - "kvmd_data/win/true.exe" post_start_cmd: - - "/bin/true" + - "kvmd_data/win/true.exe" pre_stop_cmd: - - "/bin/true" + - "kvmd_data/win/true.exe" + iface: + ip_cmd: + - "kvmd_data/win/true.exe" + firewall: + iptables_cmd: + - "kvmd_data/win/true.exe" nginx: http: @@ -123,4 +131,4 @@ nginx: janus: cmd: - - "/bin/true" \ No newline at end of file + - "kvmd_data/win/true.exe" \ No newline at end of file diff --git a/kvmd_data/usr/share/kvmd/web/kvm/index.html b/kvmd_data/usr/share/kvmd/web/kvm/index.html index f5799301..774a64bb 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 918260ab..ee75646c 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-offline") + div(id="stream-box" class="stream-box-online") 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/win/true.exe b/kvmd_data/win/true.exe new file mode 100644 index 0000000000000000000000000000000000000000..fc7475a88260f51a03a4fcb80bfae0d6d1085c5c GIT binary patch literal 128628 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 literal 0 HcmV?d00001 diff --git a/tools/list_devices.py b/tools/list_devices.py new file mode 100644 index 00000000..bb455dc6 --- /dev/null +++ b/tools/list_devices.py @@ -0,0 +1,46 @@ +import cv2 +import serial.tools.list_ports +import os +import sys + +# 隐藏 OpenCV 的错误输出 +def suppress_opencv_warnings(): + #cv2.utils.logging.setLogLevel(cv2.utils.logging.LOG_LEVEL_SILENT) + pass + +def list_video_devices(): + """列出可用的视频设备及其名称""" + video_devices = [] + for i in range(10): # 假设最多有10个视频设备 + cap = cv2.VideoCapture(i) + if cap.isOpened(): + device_name = cap.getBackendName() # 获取设备名称 + video_devices.append((i, device_name)) + cap.release() + return video_devices + +def list_serial_ports(): + """列出可用的串口设备""" + return [port.device for port in serial.tools.list_ports.comports()] + +def main(): + suppress_opencv_warnings() # 调用函数以隐藏 OpenCV 的错误输出 + + print("可用的视频设备索引及名称:") + video_devices = list_video_devices() + if video_devices: + for index, name in video_devices: + print(f"视频设备索引: {index}, 名称: {name}") + else: + print("未找到视频设备。") + + print("\n可用的串口设备:") + serial_ports = list_serial_ports() + if serial_ports: + for port in serial_ports: + print(f"串口设备: {port}") + else: + print("未找到串口设备。") + +if __name__ == "__main__": + main() diff --git a/ustreamer-win/mjpeg_stream.py b/ustreamer-win/mjpeg_stream.py new file mode 100644 index 00000000..b9b13529 --- /dev/null +++ b/ustreamer-win/mjpeg_stream.py @@ -0,0 +1,233 @@ +import asyncio +import threading +import time +import json +from collections import deque +from typing import List, Optional, Tuple, Union, Dict, Any + +import aiohttp +import cv2 +import logging +import numpy as np +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, + size: Optional[Tuple[int, int]] = None, + quality: int = 50, + fps: int = 30, + host: str = "localhost", + port: int = 8000, + 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)) + self.fps = fps + self._host = host + self._port = port + 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() + + + # 设置日志级别为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.is_running = False + + 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() + + # Add KVMD-compatible header information + headers = { + "X-UStreamer-Online": str(self._is_online).lower(), + "X-UStreamer-Width": str(frame.shape[1]), + "X-UStreamer-Height": str(frame.shape[0]), + "X-UStreamer-Name": self._device_name, + "X-Timestamp": str(int(time.time() * 1000)), + "Cache-Control": "no-store", + "Pragma": "no-cache", + "Expires": "0", + } + + return encoded, headers + + async def _stream_handler(self, request: web.Request) -> web.StreamResponse: + """Handle MJPEG stream requests""" + response = web.StreamResponse( + status=200, + reason="OK", + headers={"Content-Type": "multipart/x-mixed-replace;boundary=frame"} + ) + await response.prepare(request) + + if self.log_requests: + print(f"Stream request received: {request.path}") + + + 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 = { + "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], + "height": self.size[1] if self.size else self._frame.shape[0] + }, + "online": self._is_online, + "desired_fps": self.fps, + "captured_fps": 0 # You can update this with actual captured fps if needed + }, + "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 + } + } + } + } + } + return web.Response( + text=json.dumps(state), + content_type="application/json" + ) + + async def _index_handler(self, _: web.Request) -> web.Response: + """Handle root path requests and display available streams""" + html = f""" +

Available Video Streams:

+ + """ + return web.Response(text=html, content_type="text/html") + + 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 + print(f"\nVideo stream URL: http://{self._host}:{self._port}/{self.name}") + 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") + raise GracefulExit() + 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) + loop.run_until_complete(runner.setup()) + site = web.TCPSite(runner, self._host, self._port) + loop.run_until_complete(site.start()) + loop.run_forever() \ No newline at end of file diff --git a/ustreamer-win/ustreamer-win.py b/ustreamer-win/ustreamer-win.py new file mode 100644 index 00000000..2c894a54 --- /dev/null +++ b/ustreamer-win/ustreamer-win.py @@ -0,0 +1,197 @@ +import argparse +import cv2 +import logging +import platform +from mjpeg_stream import MjpegStream + +def configure_logging(): + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + return logging.getLogger(__name__) + +def get_windows_cameras(logger): + """Retrieve available camera devices on Windows system""" + from win32com.client import Dispatch + devices = [] + try: + wmi = Dispatch("WbemScripting.SWbemLocator") + service = wmi.ConnectServer(".", "root\\cimv2") + items = service.ExecQuery("SELECT * FROM Win32_PnPEntity WHERE (PNPClass = 'Image' OR PNPClass = 'Camera')") + + for item in items: + devices.append({ + 'name': item.Name, + 'device_id': item.DeviceID + }) + logger.info(f"Found camera device: {item.Name}") + except Exception as e: + logger.error(f"Error enumerating camera devices: {str(e)}") + return devices + +def test_camera(index, logger): + """Test if the camera is available""" + try: + cap = cv2.VideoCapture(index, cv2.CAP_DSHOW if platform.system() == "Windows" else cv2.CAP_ANY) + if cap.isOpened(): + ret, _ = cap.read() + cap.release() + return ret + except Exception as e: + logger.debug(f"Error testing camera {index}: {str(e)}") + 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 + + devices = get_windows_cameras(logger) + for device in devices: + if camera_name.lower() in device['name'].lower(): + # Try to find an available index + for i in range(5): # Usually no more than 5 devices + if test_camera(i, logger): + logger.info(f"Found matching camera '{device['name']}' at index {i}") + return i + 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 + return None + +def parse_arguments(): + parser = argparse.ArgumentParser(description='MJPEG Stream Demonstration') + device_group = parser.add_mutually_exclusive_group() + device_group.add_argument('--device', type=int, help='Camera device index') + device_group.add_argument('--device-name', type=str, help='Camera device name (only supported on Windows)') + parser.add_argument('--resolution', type=str, default='640x480', help='Video resolution (e.g., 640x480)') + parser.add_argument('--quality', type=int, default=100, help='JPEG quality (1-100)') + parser.add_argument('--fps', type=int, default=30, help='Target FPS') + parser.add_argument('--host', type=str, default='localhost', help='Server address') + 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: + raise ValueError("Resolution must be in the format WIDTHxHEIGHT (e.g., 640x480).") + + args.width = width + args.height = height + + return args + +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}'") + return + elif args.device is not None: + if test_camera(args.device, logger): + device_index = args.device + else: + logger.warning(f"The specified device index {args.device} is not available") + + if device_index is None: + device_index = get_first_available_camera(logger) + if device_index is None: + logger.error("No available camera devices were found") + return + logger.info(f"Using the first available camera device (index: {device_index})") + + # Initialize the camera + try: + cap = cv2.VideoCapture(device_index, cv2.CAP_DSHOW if platform.system() == "Windows" else cv2.CAP_ANY) + + 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 + 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") + cap.release() + return + + except Exception as e: + logger.error(f"Error initializing the camera: {str(e)}") + if 'cap' in locals(): + cap.release() + return + + # Create and start the video stream + try: + stream = MjpegStream( + name="stream", + size=(int(actual_width), int(actual_height)), # Use actual resolution + 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请求日志 + ) + stream.start() + logger.info(f"Video stream started: http://{args.host}:{args.port}/stream") + + while True: + ret, frame = cap.read() + if not ret: + logger.error("Unable to read video frames") + break + + stream.set_frame(frame) + + 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)}") + cv2.destroyAllWindows() + logger.info("Program has exited") + +if __name__ == "__main__": + main() \ No newline at end of file