From b6105450a1ac0fb9242f2cc1c72efc1f23c22eb7 Mon Sep 17 00:00:00 2001 From: nnbcccscdscdsc <2709767634@qq.com> Date: Tue, 31 Mar 2026 21:16:22 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E7=9C=9F=E5=AE=9E=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E6=B5=81=E8=BF=98=E6=B2=A1=E6=8E=A5=E9=80=9A=EF=BC=8C=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=B0=B1=E4=BC=9A=E7=A8=B3=E5=AE=9A=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E2=80=9C=E6=9C=AA=E5=AE=9E=E6=97=B6=E8=8E=B7=E5=8F=96=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E5=80=BC=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/monitoring/services.py | 79 ++++++-------------------- frontend/src/components/VideoPanel.vue | 21 ++++--- 2 files changed, 28 insertions(+), 72 deletions(-) diff --git a/backend/monitoring/services.py b/backend/monitoring/services.py index feb00c2..58277c1 100644 --- a/backend/monitoring/services.py +++ b/backend/monitoring/services.py @@ -17,10 +17,7 @@ JPEG_FRAME_DIR = WORKSPACE_ROOT / "RobotDataShow" / "jpeg-frames" # GPS 数据 JSON 文件路径 GEOSTREAM_JSON_PATH = WORKSPACE_ROOT / "GeoStream" / "gps_latest.json" GEOSTREAM_STALE_SECONDS = 15 -SAMPLE_CDATA_DIR = PROJECT_ROOT / "SampleCData" -CONFIG_DIR = PROJECT_ROOT / "config" -PRIMARY_OMNISOCKET_CONFIG_PATH = CONFIG_DIR / "omnisocket_demo.yaml" -LEGACY_OMNISOCKET_CONFIG_PATH = SAMPLE_CDATA_DIR / "config" / "omnisocket_demo.yaml" +OMNISOCKET_CONFIG_PATH = PROJECT_ROOT / "config" / "omnisocket_demo.yaml" VIDEO_SOURCE_MODE = os.getenv("VIDEO_SOURCE_MODE", "auto").strip().lower() OMNISOCKET_FRAME_FRESH_SECONDS = 2.0 @@ -29,12 +26,6 @@ def utc_iso_now() -> str: return datetime.now(UTC).isoformat(timespec="seconds").replace("+00:00", "Z") -def resolve_omnisocket_config_path() -> Path: - if PRIMARY_OMNISOCKET_CONFIG_PATH.exists(): - return PRIMARY_OMNISOCKET_CONFIG_PATH - return LEGACY_OMNISOCKET_CONFIG_PATH - - class OmniSocketVideoReceiver: def __init__(self) -> None: self._lock = threading.Lock() @@ -64,7 +55,7 @@ class OmniSocketVideoReceiver: try: from omnisocket import MSG_TYPE_BINARY, Session, VIDEO_DEFAULTS # type: ignore except ImportError: - python_dir = SAMPLE_CDATA_DIR / "python" + python_dir = WORKSPACE_ROOT / "OmniSocketGo" / "python" if python_dir.exists(): sys.path.insert(0, str(python_dir)) from omnisocket import MSG_TYPE_BINARY, Session, VIDEO_DEFAULTS # type: ignore @@ -75,8 +66,6 @@ class OmniSocketVideoReceiver: def ensure_started(self) -> None: # 当第一次请求帧或状态时,再懒启动后台接收线程。 - if VIDEO_SOURCE_MODE == "sample": - return if self._session_cls is None or self._binary_msg_type is None: return @@ -96,17 +85,16 @@ class OmniSocketVideoReceiver: # transport + video_receiver。 # 即使配置文件不存在,也允许回退到默认值继续运行。 config: dict[str, Any] = {} - config_path = resolve_omnisocket_config_path() - if config_path.exists(): + if OMNISOCKET_CONFIG_PATH.exists(): try: try: import yaml # type: ignore - with config_path.open("r", encoding="utf-8") as file: + with OMNISOCKET_CONFIG_PATH.open("r", encoding="utf-8") as file: config = yaml.safe_load(file) or {} except ImportError: - # 如果当前环境没有 PyYAML,就用一个足够支撑当前 demo 配置的简化解析器。 - config = self._load_simple_yaml_config(config_path) + # 当前配置文件结构非常简单,缺少 PyYAML 时用简化解析器兜底。 + config = self._load_simple_yaml_config(OMNISOCKET_CONFIG_PATH) except Exception as error: # pragma: no cover - 可选依赖 self._last_error = f"config load failed: {error}" @@ -247,7 +235,7 @@ class OmniSocketVideoReceiver: continue with self._lock: - # 这里只保留最新的一张 JPEG 帧,供 Web 接口直接返回给前端。 + # 缓存:这里只保留最新的一张 JPEG 帧,供 Web 接口直接返回给前端。 self._latest_frame = jpeg_frame self._latest_received_at = time.time() self._latest_sequence = self._extract_sequence(frame) @@ -277,7 +265,6 @@ class OmniSocketVideoReceiver: def get_status(self) -> dict[str, Any]: self.ensure_started() config = self._load_config() - config_path = resolve_omnisocket_config_path() transport_cfg = config.get("transport", {}) video_cfg = config.get("video_receiver", {}) with self._lock: @@ -292,7 +279,7 @@ class OmniSocketVideoReceiver: "frames_received": self._frames_received, "latest_sequence": self._latest_sequence, "last_error": self._last_error, - "config_path": str(config_path), + "config_path": str(OMNISOCKET_CONFIG_PATH), "server_addr": str(transport_cfg.get("server_addr", "")), "relay_via": str(transport_cfg.get("relay_via", "")), "peer_id": str(video_cfg.get("peer_id", "")), @@ -302,9 +289,6 @@ class OmniSocketVideoReceiver: class VideoFrameService: def __init__(self) -> None: - self._lock = threading.Lock() - self._frame_paths = sorted(JPEG_FRAME_DIR.glob("*.jpg")) - self._index = 0 self._receiver = OmniSocketVideoReceiver() def get_status(self) -> dict[str, Any]: @@ -323,31 +307,16 @@ class VideoFrameService: "receiver": receiver_status, } - # 强制实时模式时,如果还没收到真实帧,就明确告诉前端“正在等待”, - # 不再悄悄回退到本地样例图,避免调试时误以为链路已接通。 - if VIDEO_SOURCE_MODE == "omnisocket": - wait_detail = receiver_status["last_error"] or ( - "等待 OmniSocket 实时 JPEG 帧," - f"接收端 peer_id={receiver_status['peer_id']}," - f"服务器={receiver_status['server_addr']}" - ) - return { - "available": False, - "source_mode": "omnisocket-waiting", - "frame_count": receiver_status["frames_received"], - "fps": 30, - "frame_dir": str(JPEG_FRAME_DIR), - "source_detail": wait_detail, - "receiver": receiver_status, - } - + wait_detail = receiver_status["last_error"] or ( + "未实时获取真实值,请检查 OmniSocket 服务、视频发送端和接收配置。" + ) return { - "available": bool(self._frame_paths), - "source_mode": "sample-jpeg-frame-loop" if self._frame_paths else "unavailable", - "frame_count": len(self._frame_paths), + "available": False, + "source_mode": "omnisocket-waiting", + "frame_count": receiver_status["frames_received"], "fps": 30, "frame_dir": str(JPEG_FRAME_DIR), - "source_detail": receiver_status["last_error"] or "fallback to local sample frames", + "source_detail": wait_detail, "receiver": receiver_status, } @@ -357,23 +326,7 @@ class VideoFrameService: if receiver_frame is not None: return receiver_frame - # 强制实时模式下,如果还没收到真实帧,直接报错, - # 这样前端就能明确知道实时链路还没接通。 - if VIDEO_SOURCE_MODE == "omnisocket": - raise RuntimeError( - "OmniSocket 实时 JPEG 帧暂未就绪,请检查 server_addr、peer_id、" - "target_peer,以及视频发送端是否已经启动。" - ) - - # 如果当前没有真实帧,就回退到本地演示 JPEG 文件,保证页面仍然可用。 - if not self._frame_paths: - raise FileNotFoundError(f"No JPEG frames found in {JPEG_FRAME_DIR}") - - with self._lock: - frame_path = self._frame_paths[self._index] - self._index = (self._index + 1) % len(self._frame_paths) - - return frame_path.read_bytes() + raise RuntimeError("未实时获取真实值,当前没有可用的真实 JPEG 帧。") def iter_mjpeg(self, fps: float = 6.0) -> Iterator[bytes]: frame_interval = 1.0 / max(1.0, min(fps, 30.0)) diff --git a/frontend/src/components/VideoPanel.vue b/frontend/src/components/VideoPanel.vue index bd2d74a..6f43785 100644 --- a/frontend/src/components/VideoPanel.vue +++ b/frontend/src/components/VideoPanel.vue @@ -10,22 +10,25 @@ const props = defineProps<{ const frameUrl = ref(buildVideoFrameUrl(0)) const currentFps = computed(() => props.video?.fps ?? 30) -const canRequestFrames = computed(() => props.video == null || props.video.available) +const canRequestFrames = computed(() => props.video?.available === true) const modeLabel = computed(() => { if (!props.video) { - return '--' + return '正在获取视频状态' } if (props.video.source_mode === 'omnisocket-jpeg-live') { return `${props.video.fps} FPS 实时接收` } if (props.video.source_mode === 'omnisocket-waiting') { - return '等待 OmniSocket 实时帧' - } - if (props.video.source_mode === 'sample-jpeg-frame-loop') { - return `${props.video.fps} FPS 本地演示` + return '未实时获取真实值' } return `${props.video.fps} FPS` }) +const placeholderText = computed(() => { + if (!props.video) { + return '正在获取视频状态...' + } + return '未实时获取真实值' +}) let frameTimer: number | null = null let frameKey = 0 @@ -90,7 +93,7 @@ watch([currentFps, canRequestFrames], () => { alt="Robot jpeg frame stream" />
- 这里始终按固定频率逐张请求 Django 返回的单帧 JPEG,不依赖 MJPEG。只要后端已经收到 - OmniSocket 里的真实 JPEG 帧,这个组件就会直接显示实时画面。 + 这里只有在后端已经收到 OmniSocket 的真实 JPEG 帧时,才会开始逐帧请求并显示画面。 + 如果当前没有真实帧,页面会保持占位提示,不再回退测试视频流。