mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-16 19:30:23 +08:00
初步适配windows系统
This commit is contained in:
parent
4f5daebf93
commit
ddb4d752c0
@ -24,7 +24,7 @@ import asyncio
|
|||||||
import threading
|
import threading
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
|
||||||
import gpiod
|
#import gpiod
|
||||||
|
|
||||||
from . import aiotools
|
from . import aiotools
|
||||||
|
|
||||||
@ -79,46 +79,8 @@ class AioReader: # pylint: disable=too-many-instance-attributes
|
|||||||
assert self.__loop
|
assert self.__loop
|
||||||
|
|
||||||
pins = sorted(self.__pins)
|
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:
|
class _DebouncedValue:
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import signal
|
import signal
|
||||||
import asyncio
|
import asyncio
|
||||||
import asyncio.subprocess
|
import asyncio.subprocess
|
||||||
@ -38,11 +39,16 @@ async def run_process(
|
|||||||
env: (dict[str, str] | None)=None,
|
env: (dict[str, str] | None)=None,
|
||||||
) -> asyncio.subprocess.Process: # pylint: disable=no-member
|
) -> 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(
|
return (await asyncio.create_subprocess_exec(
|
||||||
*cmd,
|
*cmd,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=(asyncio.subprocess.DEVNULL if err_to_null else asyncio.subprocess.STDOUT),
|
stderr=(asyncio.subprocess.DEVNULL if err_to_null else asyncio.subprocess.STDOUT),
|
||||||
preexec_fn=os.setpgrp,
|
preexec_fn=preexec_fn,
|
||||||
env=env,
|
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:
|
def settle(name: str, suffix: str, prefix: str="kvmd") -> logging.Logger:
|
||||||
logger = get_logger(1)
|
logger = get_logger(1)
|
||||||
logger.info("Started %s pid=%d", name, os.getpid())
|
logger.info("Started %s pid=%d", name, os.getpid())
|
||||||
os.setpgrp()
|
#os.setpgrp()
|
||||||
rename_process(suffix, prefix)
|
rename_process(suffix, prefix)
|
||||||
return logger
|
return logger
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import ssl
|
|||||||
import functools
|
import functools
|
||||||
import types
|
import types
|
||||||
import typing
|
import typing
|
||||||
|
import platform
|
||||||
|
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Awaitable
|
from typing import Awaitable
|
||||||
@ -56,8 +57,9 @@ def run(coro: Coroutine, final: (Coroutine | None)=None) -> None:
|
|||||||
raise SystemExit()
|
raise SystemExit()
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.add_signal_handler(signal.SIGINT, sigint_handler)
|
if platform.system() != 'Windows':
|
||||||
loop.add_signal_handler(signal.SIGTERM, sigterm_handler)
|
loop.add_signal_handler(signal.SIGINT, sigint_handler)
|
||||||
|
loop.add_signal_handler(signal.SIGTERM, sigterm_handler)
|
||||||
|
|
||||||
main_task = loop.create_task(coro)
|
main_task = loop.create_task(coro)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -181,12 +181,13 @@ def _init_config(config_path: str, override_options: list[str], **load_flags: bo
|
|||||||
_patch_raw(raw_config)
|
_patch_raw(raw_config)
|
||||||
config = make_config(raw_config, scheme)
|
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:
|
except (ConfigError, UnknownPluginError) as ex:
|
||||||
raise SystemExit(f"ConfigError: {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
|
def _patch_raw(raw_config: dict) -> None: # pylint: disable=too-many-branches
|
||||||
|
|||||||
@ -23,6 +23,7 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import sys
|
||||||
|
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
@ -51,7 +52,7 @@ class ExtrasInfoSubmanager(BaseInfoSubmanager):
|
|||||||
sui = sysunit.SystemdUnitInfo()
|
sui = sysunit.SystemdUnitInfo()
|
||||||
await sui.open()
|
await sui.open()
|
||||||
except Exception as ex:
|
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))
|
get_logger(0).error("Can't open systemd bus to get extras state: %s", tools.efmt(ex))
|
||||||
sui = None
|
sui = None
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -169,7 +169,7 @@ class HwInfoSubmanager(BaseInfoSubmanager):
|
|||||||
+ (st.steal + st.guest) / total * 100
|
+ (st.steal + st.guest) / total * 100
|
||||||
)
|
)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
get_logger(0).error("Can't get CPU percent: %s", ex)
|
#get_logger(0).error("Can't get CPU percent: %s", ex)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def __get_mem(self) -> dict:
|
async def __get_mem(self) -> dict:
|
||||||
@ -218,7 +218,7 @@ class HwInfoSubmanager(BaseInfoSubmanager):
|
|||||||
async def __parse_vcgencmd(self, arg: str, parser: Callable[[str], _RetvalT]) -> (_RetvalT | None):
|
async def __parse_vcgencmd(self, arg: str, parser: Callable[[str], _RetvalT]) -> (_RetvalT | None):
|
||||||
cmd = [*self.__vcgencmd_cmd, arg]
|
cmd = [*self.__vcgencmd_cmd, arg]
|
||||||
try:
|
try:
|
||||||
text = (await aioproc.read_process(cmd, err_to_null=True))[1]
|
text = "throttled=0x0"
|
||||||
except Exception:
|
except Exception:
|
||||||
get_logger(0).exception("Error while executing: %s", tools.cmdfmt(cmd))
|
get_logger(0).exception("Error while executing: %s", tools.cmdfmt(cmd))
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -76,12 +76,14 @@ class SystemInfoSubmanager(BaseInfoSubmanager):
|
|||||||
except Exception:
|
except Exception:
|
||||||
get_logger(0).exception("Can't get streamer info")
|
get_logger(0).exception("Can't get streamer info")
|
||||||
else:
|
else:
|
||||||
try:
|
#try:
|
||||||
for line in features_text.split("\n"):
|
# print(features_text)
|
||||||
(status, name) = map(str.strip, line.split(" "))
|
# for line in features_text.split("\n"):
|
||||||
features[name] = (status == "+")
|
# (status, name) = map(str.strip, line.split(" "))
|
||||||
except Exception:
|
# features[name] = (status == "+")
|
||||||
get_logger(0).exception("Can't parse streamer features")
|
#except Exception:
|
||||||
|
# get_logger(0).exception("Can't parse streamer features")
|
||||||
|
pass
|
||||||
return {
|
return {
|
||||||
"app": os.path.basename(path),
|
"app": os.path.basename(path),
|
||||||
"version": version,
|
"version": version,
|
||||||
|
|||||||
@ -78,7 +78,7 @@ def _load_libtesseract() -> (ctypes.CDLL | None):
|
|||||||
setattr(func, "argtypes", argtypes)
|
setattr(func, "argtypes", argtypes)
|
||||||
return lib
|
return lib
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
warnings.warn(f"Can't load libtesseract: {ex}", RuntimeWarning)
|
#warnings.warn(f"Can't load libtesseract: {ex}", RuntimeWarning)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -38,8 +38,10 @@ from aiohttp.web import StreamResponse
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from aiohttp import ClientConnectionError
|
from aiohttp import ClientConnectionError
|
||||||
|
from aiohttp import ClientPayloadError
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
from aiohttp import UnixConnector
|
from aiohttp import UnixConnector
|
||||||
|
from aiohttp import ClientTimeout
|
||||||
from urllib.parse import urlencode
|
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()
|
socket_path = self.__streamer.get_path()
|
||||||
query_string = urlencode(request.query)
|
query_string = urlencode(request.query)
|
||||||
headers = request.headers.copy()
|
headers = request.headers.copy()
|
||||||
|
time_out = ClientTimeout(total=10)
|
||||||
try:
|
try:
|
||||||
async with ClientSession(connector=UnixConnector(path=socket_path)) as session:
|
#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() 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:
|
async with session.get(backend_url, headers=headers) as resp:
|
||||||
response = StreamResponse(status=resp.status, reason=resp.reason, headers=resp.headers)
|
response = StreamResponse(status=resp.status, reason=resp.reason, headers=resp.headers)
|
||||||
await response.prepare(request)
|
await response.prepare(request)
|
||||||
@ -341,7 +345,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
|
|||||||
break
|
break
|
||||||
await response.write(chunk)
|
await response.write(chunk)
|
||||||
return response
|
return response
|
||||||
except ClientConnectionError:
|
except (ClientConnectionError, ClientPayloadError, ConnectionResetError):
|
||||||
return Response(status=500, text="Client connection was closed")
|
return Response(status=500, text="Client connection was closed")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
# ========================================================================== #
|
# ========================================================================== #
|
||||||
|
|
||||||
|
|
||||||
|
import platform
|
||||||
import signal
|
import signal
|
||||||
import asyncio
|
import asyncio
|
||||||
import asyncio.subprocess
|
import asyncio.subprocess
|
||||||
@ -302,7 +303,8 @@ class Streamer: # pylint: disable=too-many-instance-attributes
|
|||||||
self.__notifier.notify(self.__ST_STREAMER)
|
self.__notifier.notify(self.__ST_STREAMER)
|
||||||
|
|
||||||
get_logger(0).info("Installing SIGUSR2 streamer handler ...")
|
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 = {}
|
prev: dict = {}
|
||||||
while True:
|
while True:
|
||||||
@ -338,7 +340,7 @@ class Streamer: # pylint: disable=too-many-instance-attributes
|
|||||||
session = self.__ensure_client_session()
|
session = self.__ensure_client_session()
|
||||||
try:
|
try:
|
||||||
return (await session.get_state())
|
return (await session.get_state())
|
||||||
except (aiohttp.ClientConnectionError, aiohttp.ServerConnectionError):
|
except (aiohttp.ClientConnectionError, aiohttp.ServerConnectionError,TimeoutError,asyncio.CancelledError,asyncio.TimeoutError,asyncio.CancelledError):
|
||||||
pass
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
get_logger().exception("Invalid streamer response from /state")
|
get_logger().exception("Invalid streamer response from /state")
|
||||||
|
|||||||
@ -75,12 +75,12 @@ class BaseHttpClient:
|
|||||||
def _make_http_session(self, headers: dict[str, str] | None = None) -> aiohttp.ClientSession:
|
def _make_http_session(self, headers: dict[str, str] | None = None) -> aiohttp.ClientSession:
|
||||||
connector = None
|
connector = None
|
||||||
#这里临时使用 socket ,后期考虑是否使用 http 方式
|
#这里临时使用 socket ,后期考虑是否使用 http 方式
|
||||||
use_unix_socket = True
|
use_unix_socket = False
|
||||||
if use_unix_socket:
|
if use_unix_socket:
|
||||||
connector = aiohttp.UnixConnector(path=self.__unix_path)
|
connector = aiohttp.UnixConnector(path=self.__unix_path)
|
||||||
base_url = "http://localhost:0" # 继续使用 Unix 域套接字
|
base_url = "http://localhost:0" # 继续使用 Unix 域套接字
|
||||||
else:
|
else:
|
||||||
base_url = "http://127.0.0.1:8001" # 使用指定的 IP 和端口
|
base_url = "http://127.0.0.1:8000" # 使用指定的 IP 和端口
|
||||||
|
|
||||||
#print("base_url:", base_url)
|
#print("base_url:", base_url)
|
||||||
return aiohttp.ClientSession(
|
return aiohttp.ClientSession(
|
||||||
|
|||||||
@ -32,7 +32,7 @@ from typing import Generator
|
|||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import ustreamer
|
#import ustreamer
|
||||||
|
|
||||||
from PIL import Image as PilImage
|
from PIL import Image as PilImage
|
||||||
|
|
||||||
|
|||||||
@ -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 = "--"
|
ch = "--"
|
||||||
if not ch.isprintable():
|
if not ch.isprintable():
|
||||||
continue
|
continue
|
||||||
try:
|
|
||||||
keys = symmap[_ch_to_keysym(ch)]
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for (modifiers, key) in keys.items():
|
for (modifiers, key) in keys.items():
|
||||||
if modifiers & SymmapModifiers.CTRL:
|
if modifiers & SymmapModifiers.CTRL:
|
||||||
|
|||||||
32
kvmd/libc.py
32
kvmd/libc.py
@ -34,34 +34,4 @@ from ctypes import c_void_p
|
|||||||
|
|
||||||
# =====
|
# =====
|
||||||
def _load_libc() -> ctypes.CDLL:
|
def _load_libc() -> ctypes.CDLL:
|
||||||
path = ctypes.util.find_library("c")
|
pass
|
||||||
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
|
|
||||||
|
|||||||
@ -52,8 +52,8 @@ def get_plugin_class(sub: str, name: str) -> type[BasePlugin]:
|
|||||||
assert name
|
assert name
|
||||||
if name.startswith("_"):
|
if name.startswith("_"):
|
||||||
raise UnknownPluginError(f"Unknown plugin '{sub}/{name}'")
|
raise UnknownPluginError(f"Unknown plugin '{sub}/{name}'")
|
||||||
try:
|
#try:
|
||||||
module = importlib.import_module(f"kvmd.plugins.{sub}.{name}")
|
module = importlib.import_module(f"kvmd.plugins.{sub}.{name}")
|
||||||
except ModuleNotFoundError:
|
#except ModuleNotFoundError:
|
||||||
raise UnknownPluginError(f"Unknown plugin '{sub}/{name}'")
|
#raise UnknownPluginError(f"Unknown plugin '{sub}/{name}'")
|
||||||
return getattr(module, "Plugin")
|
return getattr(module, "Plugin")
|
||||||
|
|||||||
@ -200,6 +200,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
|
|||||||
while not self.__stop_event.is_set():
|
while not self.__stop_event.is_set():
|
||||||
try:
|
try:
|
||||||
self.__hid_loop()
|
self.__hid_loop()
|
||||||
|
time.sleep(1)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Unexpected error in the run loop")
|
logger.exception("Unexpected error in the run loop")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
@ -222,6 +223,10 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
|
|||||||
self.__process_cmd(conn, b"")
|
self.__process_cmd(conn, b"")
|
||||||
else:
|
else:
|
||||||
self.__process_cmd(conn, cmd)
|
self.__process_cmd(conn, cmd)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
get_logger(0).info("KeyboardInterrupt received, exiting HID loop.")
|
||||||
|
self.clear_events()
|
||||||
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
self.clear_events()
|
self.clear_events()
|
||||||
get_logger(0).exception("Unexpected error in the HID loop")
|
get_logger(0).exception("Unexpected error in the HID loop")
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import gpiod
|
#import gpiod
|
||||||
|
|
||||||
from ... import aiotools
|
from ... import aiotools
|
||||||
from ... import aiogp
|
from ... import aiogp
|
||||||
|
|||||||
@ -26,7 +26,7 @@ kvmd:
|
|||||||
|
|
||||||
hid:
|
hid:
|
||||||
type: ch9329
|
type: ch9329
|
||||||
device: /dev/ttyUSB0
|
device: COM7
|
||||||
speed: 115200
|
speed: 115200
|
||||||
read_timeout: 0.3
|
read_timeout: 0.3
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ kvmd:
|
|||||||
|
|
||||||
streamer:
|
streamer:
|
||||||
resolution:
|
resolution:
|
||||||
default: 1920x1080
|
default: 1280x720
|
||||||
|
|
||||||
forever: true
|
forever: true
|
||||||
|
|
||||||
@ -64,32 +64,30 @@ kvmd:
|
|||||||
h264_bitrate:
|
h264_bitrate:
|
||||||
default: 8000
|
default: 8000
|
||||||
|
|
||||||
cmd:
|
pre_start_cmd: kvmd_data/win/true.exe
|
||||||
- "kvmd_data/usr/bin/ustreamer"
|
post_stop_cmd: kvmd_data/win/true.exe
|
||||||
- "--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"
|
|
||||||
|
|
||||||
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:
|
ipmi:
|
||||||
auth:
|
auth:
|
||||||
file: kvmd_data/etc/kvmd/ipmipasswd
|
file: kvmd_data/etc/kvmd/ipmipasswd
|
||||||
|
|
||||||
|
pst:
|
||||||
|
remount_cmd:
|
||||||
|
- "kvmd_data/win/true.exe"
|
||||||
|
|
||||||
|
|
||||||
vnc:
|
vnc:
|
||||||
keymap: kvmd_data/usr/share/kvmd/keymaps/en-us
|
keymap: kvmd_data/usr/share/kvmd/keymaps/en-us
|
||||||
mouse_output: usb
|
mouse_output: usb
|
||||||
@ -110,10 +108,20 @@ vnc:
|
|||||||
|
|
||||||
otgnet:
|
otgnet:
|
||||||
commands:
|
commands:
|
||||||
|
pre_start_cmd:
|
||||||
|
- "kvmd_data/win/true.exe"
|
||||||
|
post_stop_cmd:
|
||||||
|
- "kvmd_data/win/true.exe"
|
||||||
post_start_cmd:
|
post_start_cmd:
|
||||||
- "/bin/true"
|
- "kvmd_data/win/true.exe"
|
||||||
pre_stop_cmd:
|
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:
|
nginx:
|
||||||
http:
|
http:
|
||||||
@ -123,4 +131,4 @@ nginx:
|
|||||||
|
|
||||||
janus:
|
janus:
|
||||||
cmd:
|
cmd:
|
||||||
- "/bin/true"
|
- "kvmd_data/win/true.exe"
|
||||||
@ -899,7 +899,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="stream-info"></div>
|
<div id="stream-info"></div>
|
||||||
<button class="window-button-exit-full-tab">▼</button>
|
<button class="window-button-exit-full-tab">▼</button>
|
||||||
<div class="stream-box-offline" id="stream-box"><img id="stream-image" src="/share/png/blank-stream.png">
|
<div class="stream-box-online" id="stream-box"><img id="stream-image" src="/share/png/blank-stream.png">
|
||||||
<video class="hidden" id="stream-video" disablePictureInPicture="true" autoplay playsinline muted></video>
|
<video class="hidden" id="stream-video" disablePictureInPicture="true" autoplay playsinline muted></video>
|
||||||
<div id="stream-fullscreen-active"></div>
|
<div id="stream-fullscreen-active"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -13,7 +13,7 @@ div(id="stream-window" class="window window-resizable")
|
|||||||
div(id="stream-info")
|
div(id="stream-info")
|
||||||
|
|
||||||
button(class="window-button-exit-full-tab") ▼
|
button(class="window-button-exit-full-tab") ▼
|
||||||
div(id="stream-box" class="stream-box-offline")
|
div(id="stream-box" class="stream-box-online")
|
||||||
img(id="stream-image" src=`${png_dir}/blank-stream.png`)
|
img(id="stream-image" src=`${png_dir}/blank-stream.png`)
|
||||||
video(id="stream-video" class="hidden" disablePictureInPicture="true" autoplay playsinline muted)
|
video(id="stream-video" class="hidden" disablePictureInPicture="true" autoplay playsinline muted)
|
||||||
div(id="stream-fullscreen-active")
|
div(id="stream-fullscreen-active")
|
||||||
|
|||||||
BIN
kvmd_data/win/true.exe
Normal file
BIN
kvmd_data/win/true.exe
Normal file
Binary file not shown.
46
tools/list_devices.py
Normal file
46
tools/list_devices.py
Normal file
@ -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()
|
||||||
233
ustreamer-win/mjpeg_stream.py
Normal file
233
ustreamer-win/mjpeg_stream.py
Normal file
@ -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"""
|
||||||
|
<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>
|
||||||
|
</ul>
|
||||||
|
"""
|
||||||
|
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()
|
||||||
197
ustreamer-win/ustreamer-win.py
Normal file
197
ustreamer-win/ustreamer-win.py
Normal file
@ -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()
|
||||||
Loading…
x
Reference in New Issue
Block a user