fix:真实视频流还没接通,页面就会稳定显示“未实时获取真实值”
This commit is contained in:
@@ -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,13 +307,8 @@ class VideoFrameService:
|
|||||||
"receiver": receiver_status,
|
"receiver": receiver_status,
|
||||||
}
|
}
|
||||||
|
|
||||||
# 强制实时模式时,如果还没收到真实帧,就明确告诉前端“正在等待”,
|
|
||||||
# 不再悄悄回退到本地样例图,避免调试时误以为链路已接通。
|
|
||||||
if VIDEO_SOURCE_MODE == "omnisocket":
|
|
||||||
wait_detail = receiver_status["last_error"] or (
|
wait_detail = receiver_status["last_error"] or (
|
||||||
"等待 OmniSocket 实时 JPEG 帧,"
|
"未实时获取真实值,请检查 OmniSocket 服务、视频发送端和接收配置。"
|
||||||
f"接收端 peer_id={receiver_status['peer_id']},"
|
|
||||||
f"服务器={receiver_status['server_addr']}"
|
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"available": False,
|
"available": False,
|
||||||
@@ -341,39 +320,13 @@ class VideoFrameService:
|
|||||||
"receiver": receiver_status,
|
"receiver": receiver_status,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
"available": bool(self._frame_paths),
|
|
||||||
"source_mode": "sample-jpeg-frame-loop" if self._frame_paths else "unavailable",
|
|
||||||
"frame_count": len(self._frame_paths),
|
|
||||||
"fps": 30,
|
|
||||||
"frame_dir": str(JPEG_FRAME_DIR),
|
|
||||||
"source_detail": receiver_status["last_error"] or "fallback to local sample frames",
|
|
||||||
"receiver": receiver_status,
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_next_frame(self) -> bytes:
|
def get_next_frame(self) -> bytes:
|
||||||
# 优先返回从 Python/C 视频接收器拿到的最新真实 JPEG 帧。
|
# 优先返回从 Python/C 视频接收器拿到的最新真实 JPEG 帧。
|
||||||
receiver_frame = self._receiver.get_latest_frame()
|
receiver_frame = self._receiver.get_latest_frame()
|
||||||
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))
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user