fix:真实视频流还没接通,页面就会稳定显示“未实时获取真实值”

This commit is contained in:
nnbcccscdscdsc
2026-03-31 21:16:22 +08:00
parent dcb50219dd
commit b6105450a1
2 changed files with 28 additions and 72 deletions

View File

@@ -17,10 +17,7 @@ JPEG_FRAME_DIR = WORKSPACE_ROOT / "RobotDataShow" / "jpeg-frames"
# GPS 数据 JSON 文件路径 # GPS 数据 JSON 文件路径
GEOSTREAM_JSON_PATH = WORKSPACE_ROOT / "GeoStream" / "gps_latest.json" GEOSTREAM_JSON_PATH = WORKSPACE_ROOT / "GeoStream" / "gps_latest.json"
GEOSTREAM_STALE_SECONDS = 15 GEOSTREAM_STALE_SECONDS = 15
SAMPLE_CDATA_DIR = PROJECT_ROOT / "SampleCData" OMNISOCKET_CONFIG_PATH = PROJECT_ROOT / "config" / "omnisocket_demo.yaml"
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"
VIDEO_SOURCE_MODE = os.getenv("VIDEO_SOURCE_MODE", "auto").strip().lower() VIDEO_SOURCE_MODE = os.getenv("VIDEO_SOURCE_MODE", "auto").strip().lower()
OMNISOCKET_FRAME_FRESH_SECONDS = 2.0 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") 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: class OmniSocketVideoReceiver:
def __init__(self) -> None: def __init__(self) -> None:
self._lock = threading.Lock() self._lock = threading.Lock()
@@ -64,7 +55,7 @@ class OmniSocketVideoReceiver:
try: try:
from omnisocket import MSG_TYPE_BINARY, Session, VIDEO_DEFAULTS # type: ignore from omnisocket import MSG_TYPE_BINARY, Session, VIDEO_DEFAULTS # type: ignore
except ImportError: except ImportError:
python_dir = SAMPLE_CDATA_DIR / "python" python_dir = WORKSPACE_ROOT / "OmniSocketGo" / "python"
if python_dir.exists(): if python_dir.exists():
sys.path.insert(0, str(python_dir)) sys.path.insert(0, str(python_dir))
from omnisocket import MSG_TYPE_BINARY, Session, VIDEO_DEFAULTS # type: ignore from omnisocket import MSG_TYPE_BINARY, Session, VIDEO_DEFAULTS # type: ignore
@@ -75,8 +66,6 @@ class OmniSocketVideoReceiver:
def ensure_started(self) -> None: def ensure_started(self) -> None:
# 当第一次请求帧或状态时,再懒启动后台接收线程。 # 当第一次请求帧或状态时,再懒启动后台接收线程。
if VIDEO_SOURCE_MODE == "sample":
return
if self._session_cls is None or self._binary_msg_type is None: if self._session_cls is None or self._binary_msg_type is None:
return return
@@ -96,17 +85,16 @@ class OmniSocketVideoReceiver:
# transport + video_receiver。 # transport + video_receiver。
# 即使配置文件不存在,也允许回退到默认值继续运行。 # 即使配置文件不存在,也允许回退到默认值继续运行。
config: dict[str, Any] = {} config: dict[str, Any] = {}
config_path = resolve_omnisocket_config_path() if OMNISOCKET_CONFIG_PATH.exists():
if config_path.exists():
try: try:
try: try:
import yaml # type: ignore 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 {} config = yaml.safe_load(file) or {}
except ImportError: except ImportError:
# 如果当前环境没有 PyYAML就用一个足够支撑当前 demo 配置的简化解析器。 # 当前配置文件结构非常简单,缺少 PyYAML 时用简化解析器兜底
config = self._load_simple_yaml_config(config_path) config = self._load_simple_yaml_config(OMNISOCKET_CONFIG_PATH)
except Exception as error: # pragma: no cover - 可选依赖 except Exception as error: # pragma: no cover - 可选依赖
self._last_error = f"config load failed: {error}" self._last_error = f"config load failed: {error}"
@@ -247,7 +235,7 @@ class OmniSocketVideoReceiver:
continue continue
with self._lock: with self._lock:
# 这里只保留最新的一张 JPEG 帧,供 Web 接口直接返回给前端。 # 缓存:这里只保留最新的一张 JPEG 帧,供 Web 接口直接返回给前端。
self._latest_frame = jpeg_frame self._latest_frame = jpeg_frame
self._latest_received_at = time.time() self._latest_received_at = time.time()
self._latest_sequence = self._extract_sequence(frame) self._latest_sequence = self._extract_sequence(frame)
@@ -277,7 +265,6 @@ class OmniSocketVideoReceiver:
def get_status(self) -> dict[str, Any]: def get_status(self) -> dict[str, Any]:
self.ensure_started() self.ensure_started()
config = self._load_config() config = self._load_config()
config_path = resolve_omnisocket_config_path()
transport_cfg = config.get("transport", {}) transport_cfg = config.get("transport", {})
video_cfg = config.get("video_receiver", {}) video_cfg = config.get("video_receiver", {})
with self._lock: with self._lock:
@@ -292,7 +279,7 @@ class OmniSocketVideoReceiver:
"frames_received": self._frames_received, "frames_received": self._frames_received,
"latest_sequence": self._latest_sequence, "latest_sequence": self._latest_sequence,
"last_error": self._last_error, "last_error": self._last_error,
"config_path": str(config_path), "config_path": str(OMNISOCKET_CONFIG_PATH),
"server_addr": str(transport_cfg.get("server_addr", "")), "server_addr": str(transport_cfg.get("server_addr", "")),
"relay_via": str(transport_cfg.get("relay_via", "")), "relay_via": str(transport_cfg.get("relay_via", "")),
"peer_id": str(video_cfg.get("peer_id", "")), "peer_id": str(video_cfg.get("peer_id", "")),
@@ -302,9 +289,6 @@ class OmniSocketVideoReceiver:
class VideoFrameService: class VideoFrameService:
def __init__(self) -> None: def __init__(self) -> None:
self._lock = threading.Lock()
self._frame_paths = sorted(JPEG_FRAME_DIR.glob("*.jpg"))
self._index = 0
self._receiver = OmniSocketVideoReceiver() self._receiver = OmniSocketVideoReceiver()
def get_status(self) -> dict[str, Any]: def get_status(self) -> dict[str, Any]:
@@ -323,31 +307,16 @@ class VideoFrameService:
"receiver": receiver_status, "receiver": receiver_status,
} }
# 强制实时模式时,如果还没收到真实帧,就明确告诉前端“正在等待”, wait_detail = receiver_status["last_error"] or (
# 不再悄悄回退到本地样例图,避免调试时误以为链路已接通。 "未实时获取真实值,请检查 OmniSocket 服务、视频发送端和接收配置。"
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,
}
return { return {
"available": bool(self._frame_paths), "available": False,
"source_mode": "sample-jpeg-frame-loop" if self._frame_paths else "unavailable", "source_mode": "omnisocket-waiting",
"frame_count": len(self._frame_paths), "frame_count": receiver_status["frames_received"],
"fps": 30, "fps": 30,
"frame_dir": str(JPEG_FRAME_DIR), "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, "receiver": receiver_status,
} }
@@ -357,23 +326,7 @@ class VideoFrameService:
if receiver_frame is not None: if receiver_frame is not None:
return receiver_frame return receiver_frame
# 强制实时模式下,如果还没收到真实帧,直接报错, raise RuntimeError("未实时获取真实值,当前没有可用的真实 JPEG 帧。")
# 这样前端就能明确知道实时链路还没接通。
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()
def iter_mjpeg(self, fps: float = 6.0) -> Iterator[bytes]: def iter_mjpeg(self, fps: float = 6.0) -> Iterator[bytes]:
frame_interval = 1.0 / max(1.0, min(fps, 30.0)) frame_interval = 1.0 / max(1.0, min(fps, 30.0))

View File

@@ -10,22 +10,25 @@ const props = defineProps<{
const frameUrl = ref(buildVideoFrameUrl(0)) const frameUrl = ref(buildVideoFrameUrl(0))
const currentFps = computed(() => props.video?.fps ?? 30) 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(() => { const modeLabel = computed(() => {
if (!props.video) { if (!props.video) {
return '--' return '正在获取视频状态'
} }
if (props.video.source_mode === 'omnisocket-jpeg-live') { if (props.video.source_mode === 'omnisocket-jpeg-live') {
return `${props.video.fps} FPS 实时接收` return `${props.video.fps} FPS 实时接收`
} }
if (props.video.source_mode === 'omnisocket-waiting') { if (props.video.source_mode === 'omnisocket-waiting') {
return '等待 OmniSocket 实时帧' return '未实时获取真实值'
}
if (props.video.source_mode === 'sample-jpeg-frame-loop') {
return `${props.video.fps} FPS 本地演示`
} }
return `${props.video.fps} FPS` return `${props.video.fps} FPS`
}) })
const placeholderText = computed(() => {
if (!props.video) {
return '正在获取视频状态...'
}
return '未实时获取真实值'
})
let frameTimer: number | null = null let frameTimer: number | null = null
let frameKey = 0 let frameKey = 0
@@ -90,7 +93,7 @@ watch([currentFps, canRequestFrames], () => {
alt="Robot jpeg frame stream" alt="Robot jpeg frame stream"
/> />
<div v-else class="video-placeholder"> <div v-else class="video-placeholder">
正在等待 OmniSocket 实时 JPEG 帧接入... {{ placeholderText }}
</div> </div>
</div> </div>
@@ -106,8 +109,8 @@ watch([currentFps, canRequestFrames], () => {
</div> </div>
<p class="hint"> <p class="hint">
这里始终按固定频率逐张请求 Django 返回的单帧 JPEG不依赖 MJPEG只要后端已经收到 这里只有在后端已经收到 OmniSocket 的真实 JPEG 帧时才会开始逐帧请求并显示画面
OmniSocket 里的真实 JPEG 这个组件就会直接显示实时画面 如果当前没有真实帧页面会保持占位提示不再回退测试视频流
</p> </p>
<p class="hint subtle"> <p class="hint subtle">