feat:计算视频帧传来的差值

This commit is contained in:
nnbcccscdscdsc
2026-04-02 15:29:49 +08:00
parent 1e828cc036
commit 51ea86e887
6 changed files with 294 additions and 26 deletions

View File

@@ -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: