fix:真实视频流还没接通,页面就会稳定显示“未实时获取真实值”
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
<div v-else class="video-placeholder">
|
||||
正在等待 OmniSocket 实时 JPEG 帧接入...
|
||||
{{ placeholderText }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -106,8 +109,8 @@ watch([currentFps, canRequestFrames], () => {
|
||||
</div>
|
||||
|
||||
<p class="hint">
|
||||
这里始终按固定频率逐张请求 Django 返回的单帧 JPEG,不依赖 MJPEG。只要后端已经收到
|
||||
OmniSocket 里的真实 JPEG 帧,这个组件就会直接显示实时画面。
|
||||
这里只有在后端已经收到 OmniSocket 的真实 JPEG 帧时,才会开始逐帧请求并显示画面。
|
||||
如果当前没有真实帧,页面会保持占位提示,不再回退测试视频流。
|
||||
</p>
|
||||
|
||||
<p class="hint subtle">
|
||||
|
||||
Reference in New Issue
Block a user