build 115 - кнопка в эфире будет загораться
This commit is contained in:
@@ -257,8 +257,12 @@ class VmixAgentHub:
|
||||
self._auto_refresh_task: asyncio.Task[None] | None = None
|
||||
self._auto_refresh_next: dict[tuple[str, str], float] = {}
|
||||
self._mapping_value_cache: dict[tuple[str, str, int, str, str, str, str], str] = {}
|
||||
# BUILD83: server-side mirror of ACK-confirmed runtime Overlay state.
|
||||
# BUILD83: server-side mirror of runtime Overlay state.
|
||||
self._runtime_overlay_state: dict[str, dict[str, dict[str, str]]] = {}
|
||||
# BUILD115: true when Overlay state comes from the Agent reading the actual
|
||||
# local vMix XML API rather than from optimistic command history.
|
||||
self._runtime_overlay_authoritative: dict[str, bool] = {}
|
||||
self._runtime_overlay_observed_at: dict[str, str] = {}
|
||||
self._auto_refresh_last_error = ""
|
||||
|
||||
@staticmethod
|
||||
@@ -292,6 +296,11 @@ class VmixAgentHub:
|
||||
parsed = self._runtime_overlay_command(command.get("Function") or command.get("function"))
|
||||
if parsed is None:
|
||||
return
|
||||
# A command only tells us what we asked vMix to do. Until the Agent reads
|
||||
# the XML API again, this local mirror is optimistic rather than a tally.
|
||||
if not hasattr(self, "_runtime_overlay_authoritative"):
|
||||
self._runtime_overlay_authoritative = {}
|
||||
self._runtime_overlay_authoritative[device_id] = False
|
||||
layer, action = parsed
|
||||
device_state = self._runtime_overlay_state.setdefault(device_id, {})
|
||||
if layer == "all":
|
||||
@@ -325,7 +334,15 @@ class VmixAgentHub:
|
||||
def _runtime_overlay_payload(self, device_id: str) -> dict[str, Any]:
|
||||
source = self._runtime_overlay_state.get(device_id, {})
|
||||
overlays = {str(layer): dict(value) for layer, value in source.items() if str(layer) in {"1", "2", "3", "4"}}
|
||||
return {"device_id": device_id, "overlays": overlays}
|
||||
authoritative = bool(getattr(self, "_runtime_overlay_authoritative", {}).get(device_id, False))
|
||||
observed = str(getattr(self, "_runtime_overlay_observed_at", {}).get(device_id) or "")
|
||||
return {
|
||||
"device_id": device_id,
|
||||
"overlays": overlays,
|
||||
"authoritative": authoritative,
|
||||
"source": "agent_tally" if authoritative else "command_mirror",
|
||||
"observed_at": observed,
|
||||
}
|
||||
|
||||
def runtime_overlay_state_for_user(self, user: HockeyUser, *, device_id: str = "") -> dict[str, Any]:
|
||||
requested = self.normalise_device_id(device_id) if str(device_id or "").strip() else ""
|
||||
@@ -650,6 +667,8 @@ class VmixAgentHub:
|
||||
current = self._live.get(device_id)
|
||||
if current is not None and current.websocket is websocket:
|
||||
self._live.pop(device_id, None)
|
||||
self._runtime_overlay_state.pop(device_id, None)
|
||||
self._runtime_overlay_authoritative[device_id] = False
|
||||
with self.database.session() as session:
|
||||
row = session.scalar(select(VmixDevice).where(VmixDevice.device_uuid == device_id))
|
||||
if row is not None:
|
||||
@@ -667,6 +686,7 @@ class VmixAgentHub:
|
||||
row.vmix_connected = bool(vmix.get("connected"))
|
||||
if not row.vmix_connected:
|
||||
self._runtime_overlay_state.pop(device_id, None)
|
||||
self._runtime_overlay_authoritative[device_id] = False
|
||||
if vmix.get("version") is not None:
|
||||
row.vmix_version = str(vmix.get("version") or "")[:64]
|
||||
if vmix.get("url") is not None:
|
||||
@@ -674,6 +694,46 @@ class VmixAgentHub:
|
||||
if message.get("error") is not None:
|
||||
row.last_error = str(message.get("error") or "")[:1000]
|
||||
|
||||
async def receive_overlay_state(self, device_id: str, message: dict[str, Any]) -> None:
|
||||
raw_overlays = message.get("overlays") if isinstance(message.get("overlays"), dict) else {}
|
||||
next_state: dict[str, dict[str, str]] = {}
|
||||
for layer in ("1", "2", "3", "4"):
|
||||
raw = raw_overlays.get(layer)
|
||||
if raw is None:
|
||||
raw = raw_overlays.get(int(layer))
|
||||
if not isinstance(raw, dict):
|
||||
continue
|
||||
entry = {
|
||||
"input": str(raw.get("input") or "").strip(),
|
||||
"input_number": str(raw.get("input_number") or "").strip(),
|
||||
"input_key": str(raw.get("input_key") or "").strip(),
|
||||
"input_title": str(raw.get("input_title") or "").strip(),
|
||||
"sequence_id": "",
|
||||
"sequence_name": "",
|
||||
"button_id": "",
|
||||
"source": "agent_tally",
|
||||
}
|
||||
if not any(entry.get(key) for key in ("input", "input_number", "input_key", "input_title")):
|
||||
continue
|
||||
next_state[layer] = entry
|
||||
|
||||
vmix_connected = bool(message.get("vmix_connected", True))
|
||||
if vmix_connected:
|
||||
self._runtime_overlay_state[device_id] = next_state
|
||||
self._runtime_overlay_authoritative[device_id] = True
|
||||
self._runtime_overlay_observed_at[device_id] = str(message.get("observed_at") or _utcnow().isoformat())[:80]
|
||||
else:
|
||||
self._runtime_overlay_state.pop(device_id, None)
|
||||
self._runtime_overlay_authoritative[device_id] = False
|
||||
self._runtime_overlay_observed_at[device_id] = str(message.get("observed_at") or _utcnow().isoformat())[:80]
|
||||
|
||||
now = _utcnow()
|
||||
with self.database.session() as session:
|
||||
row = session.scalar(select(VmixDevice).where(VmixDevice.device_uuid == device_id))
|
||||
if row is not None:
|
||||
row.last_seen_at = now
|
||||
row.vmix_connected = vmix_connected
|
||||
|
||||
async def send(self, device_id: str, payload: dict[str, Any]) -> bool:
|
||||
async with self._lock:
|
||||
live = self._live.get(device_id)
|
||||
@@ -4558,8 +4618,11 @@ def create_hockey_agent_router(
|
||||
if not isinstance(message, dict):
|
||||
continue
|
||||
message_type = str(message.get("type") or "")
|
||||
if message_type in {"heartbeat", "vmix.status", "command.ack", "command.batch.ack", "match.accepted", "vmix.inventory"}:
|
||||
await hub.receive_status(device_id, message)
|
||||
if message_type in {"heartbeat", "vmix.status", "command.ack", "command.batch.ack", "match.accepted", "vmix.inventory", "vmix.overlay_state"}:
|
||||
if message_type == "vmix.overlay_state":
|
||||
await hub.receive_overlay_state(device_id, message)
|
||||
else:
|
||||
await hub.receive_status(device_id, message)
|
||||
if message_type == "vmix.inventory":
|
||||
await hub.receive_inventory(device_id, message)
|
||||
if message_type in {"command.ack", "command.batch.ack"}:
|
||||
|
||||
Reference in New Issue
Block a user