feat:计算视频帧传来的差值
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
@@ -20,6 +21,8 @@ GEOSTREAM_STALE_SECONDS = 15
|
||||
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
|
||||
VIDEO_TIMESTAMP_SAMPLE_SIZE = 10
|
||||
VIDEO_TIMESTAMP_MAX_SKEW_NS = 7 * 24 * 60 * 60 * 1_000_000_000
|
||||
|
||||
|
||||
def utc_iso_now() -> str:
|
||||
@@ -38,6 +41,10 @@ class OmniSocketVideoReceiver:
|
||||
self._latest_frame: bytes | None = None
|
||||
self._latest_received_at = 0.0
|
||||
self._latest_sequence: int | None = None
|
||||
self._latest_latency_ms: float | None = None
|
||||
self._latest_timestamp_unit: str | None = None
|
||||
self._latest_timestamp_endianness: str | None = None
|
||||
self._latency_samples_ms: deque[float] = deque(maxlen=VIDEO_TIMESTAMP_SAMPLE_SIZE)
|
||||
self._frames_received = 0
|
||||
self._last_error = ""
|
||||
self._load_backend()
|
||||
@@ -194,7 +201,7 @@ class OmniSocketVideoReceiver:
|
||||
)
|
||||
return session, int(video_cfg.get("buffer_bytes", 1024 * 1024))
|
||||
|
||||
def _extract_jpeg_frame(self, frame: bytes) -> bytes | None:
|
||||
def _extract_jpeg_payload(self, frame: bytes) -> bytes | None:
|
||||
# 同时兼容两种帧格式:
|
||||
# 1. 纯 JPEG 二进制
|
||||
# 2. 前 8 字节是序号,后面才是真正的 JPEG 数据
|
||||
@@ -204,11 +211,68 @@ class OmniSocketVideoReceiver:
|
||||
return frame[8:]
|
||||
return None
|
||||
|
||||
def _extract_jpeg_frame(self, frame: bytes) -> bytes | None:
|
||||
jpeg_payload = self._extract_jpeg_payload(frame)
|
||||
if jpeg_payload is None:
|
||||
return None
|
||||
|
||||
eoi_index = jpeg_payload.rfind(b"\xff\xd9")
|
||||
if eoi_index < 0:
|
||||
return jpeg_payload
|
||||
|
||||
return jpeg_payload[: eoi_index + 2]
|
||||
|
||||
def _extract_sequence(self, frame: bytes) -> int | None:
|
||||
if len(frame) >= 8 and not frame.startswith(b"\xff\xd8"):
|
||||
return int.from_bytes(frame[:8], "big")
|
||||
return None
|
||||
|
||||
def _extract_frame_tail(self, frame: bytes) -> bytes:
|
||||
jpeg_payload = self._extract_jpeg_payload(frame)
|
||||
if jpeg_payload is None:
|
||||
return b""
|
||||
|
||||
eoi_index = jpeg_payload.rfind(b"\xff\xd9")
|
||||
if eoi_index < 0:
|
||||
return b""
|
||||
|
||||
trailer_start = eoi_index + 2
|
||||
if trailer_start >= len(jpeg_payload):
|
||||
return b""
|
||||
|
||||
return jpeg_payload[trailer_start:]
|
||||
|
||||
def _extract_frame_timestamp(self, frame: bytes) -> tuple[int, str, str] | None:
|
||||
trailer = self._extract_frame_tail(frame)
|
||||
if len(trailer) < 8:
|
||||
return None
|
||||
|
||||
now_ns = time.time_ns()
|
||||
raw_timestamp = trailer[-8:]
|
||||
best_candidate: tuple[int, str, str] | None = None
|
||||
best_distance_ns: int | None = None
|
||||
|
||||
for endianness in ("big", "little"):
|
||||
value = int.from_bytes(raw_timestamp, endianness, signed=True)
|
||||
if value <= 0:
|
||||
continue
|
||||
|
||||
for unit, multiplier in (
|
||||
("ns", 1),
|
||||
("us", 1_000),
|
||||
("ms", 1_000_000),
|
||||
("s", 1_000_000_000),
|
||||
):
|
||||
timestamp_ns = value * multiplier
|
||||
distance_ns = abs(now_ns - timestamp_ns)
|
||||
if distance_ns > VIDEO_TIMESTAMP_MAX_SKEW_NS:
|
||||
continue
|
||||
if best_distance_ns is None or distance_ns < best_distance_ns:
|
||||
best_distance_ns = distance_ns
|
||||
best_candidate = (timestamp_ns, unit, endianness)
|
||||
|
||||
return best_candidate
|
||||
|
||||
def _run(self) -> None:
|
||||
# 后台持续接收循环:
|
||||
# connect -> recv_into(buffer) -> 按 body_len 截出有效内容 -> 把最新 JPEG 帧缓存在内存里
|
||||
@@ -234,11 +298,25 @@ class OmniSocketVideoReceiver:
|
||||
self._last_error = "received non-JPEG binary frame"
|
||||
continue
|
||||
|
||||
timestamp_meta = self._extract_frame_timestamp(frame)
|
||||
latency_ms = None
|
||||
if timestamp_meta is not None:
|
||||
timestamp_ns, unit, endianness = timestamp_meta
|
||||
latency_ms = round((time.time_ns() - timestamp_ns) / 1_000_000, 3)
|
||||
else:
|
||||
unit = None
|
||||
endianness = None
|
||||
|
||||
with self._lock:
|
||||
# 缓存:这里只保留最新的一张 JPEG 帧,供 Web 接口直接返回给前端。
|
||||
self._latest_frame = jpeg_frame
|
||||
self._latest_received_at = time.time()
|
||||
self._latest_sequence = self._extract_sequence(frame)
|
||||
self._latest_latency_ms = latency_ms
|
||||
self._latest_timestamp_unit = unit
|
||||
self._latest_timestamp_endianness = endianness
|
||||
if latency_ms is not None:
|
||||
self._latency_samples_ms.append(latency_ms)
|
||||
self._frames_received += 1
|
||||
except Exception as error: # pragma: no cover - 运行时集成路径
|
||||
self._last_error = str(error)
|
||||
@@ -271,6 +349,26 @@ class OmniSocketVideoReceiver:
|
||||
has_recent_frame = self._latest_frame is not None and (
|
||||
time.time() - self._latest_received_at <= OMNISOCKET_FRAME_FRESH_SECONDS
|
||||
)
|
||||
if has_recent_frame and self._latest_latency_ms is not None:
|
||||
timing_status = {
|
||||
"available": True,
|
||||
"latest_delta_ms": self._latest_latency_ms,
|
||||
"delta_samples_ms": list(reversed(self._latency_samples_ms)),
|
||||
"sample_count": len(self._latency_samples_ms),
|
||||
"sample_window_size": VIDEO_TIMESTAMP_SAMPLE_SIZE,
|
||||
"timestamp_unit": self._latest_timestamp_unit,
|
||||
"timestamp_endianness": self._latest_timestamp_endianness,
|
||||
}
|
||||
else:
|
||||
timing_status = {
|
||||
"available": False,
|
||||
"latest_delta_ms": None,
|
||||
"delta_samples_ms": [],
|
||||
"sample_count": 0,
|
||||
"sample_window_size": VIDEO_TIMESTAMP_SAMPLE_SIZE,
|
||||
"timestamp_unit": None,
|
||||
"timestamp_endianness": None,
|
||||
}
|
||||
return {
|
||||
"backend_ready": self._session_cls is not None,
|
||||
"mode": VIDEO_SOURCE_MODE,
|
||||
@@ -284,6 +382,7 @@ class OmniSocketVideoReceiver:
|
||||
"relay_via": str(transport_cfg.get("relay_via", "")),
|
||||
"peer_id": str(video_cfg.get("peer_id", "")),
|
||||
"buffer_bytes": int(video_cfg.get("buffer_bytes", 0)),
|
||||
"timing": timing_status,
|
||||
}
|
||||
|
||||
|
||||
@@ -305,6 +404,7 @@ class VideoFrameService:
|
||||
"frame_dir": str(JPEG_FRAME_DIR),
|
||||
"source_detail": f"peer stream active, frames={receiver_status['frames_received']}",
|
||||
"receiver": receiver_status,
|
||||
"timing": receiver_status["timing"],
|
||||
}
|
||||
|
||||
wait_detail = receiver_status["last_error"] or (
|
||||
@@ -318,6 +418,7 @@ class VideoFrameService:
|
||||
"frame_dir": str(JPEG_FRAME_DIR),
|
||||
"source_detail": wait_detail,
|
||||
"receiver": receiver_status,
|
||||
"timing": receiver_status["timing"],
|
||||
}
|
||||
|
||||
def get_next_frame(self) -> bytes:
|
||||
|
||||
Reference in New Issue
Block a user