поправлены способ передачи таймеров в vMix

This commit is contained in:
2026-08-20 15:33:03 +03:00
parent 8790a1e113
commit cd9476d167
6 changed files with 420 additions and 117 deletions

View File

@@ -238,6 +238,9 @@ class VmixAgentHub:
self._live: dict[str, LiveAgent] = {}
self._lock = asyncio.Lock()
self._pending_commands: dict[str, tuple[str, asyncio.Future[dict[str, Any]]]] = {}
# BUILD90: one FIFO lock per Agent. Shortcut batches, Mapping refreshes and
# timer control commands can no longer interleave on the same vMix instance.
self._vmix_send_locks: dict[str, asyncio.Lock] = {}
self.mapping_data = MappingDataService(database, settings=settings)
self._auto_refresh_task: asyncio.Task[None] | None = None
self._auto_refresh_next: dict[tuple[str, str], float] = {}
@@ -663,6 +666,13 @@ class VmixAgentHub:
self._live.pop(device_id, None)
return False
def _device_vmix_send_lock(self, device_id: str) -> asyncio.Lock:
lock = self._vmix_send_locks.get(device_id)
if lock is None:
lock = asyncio.Lock()
self._vmix_send_locks[device_id] = lock
return lock
async def receive_command_ack(self, device_id: str, message: dict[str, Any]) -> None:
request_id = str(message.get("request_id") or "").strip()
if not request_id:
@@ -677,7 +687,7 @@ class VmixAgentHub:
payload["device_id"] = device_id
future.set_result(payload)
async def send_vmix_command(
async def _send_vmix_command_unlocked(
self,
device_id: str,
*,
@@ -717,7 +727,21 @@ class VmixAgentHub:
if not pending_future.done():
pending_future.cancel()
async def send_vmix_batch(
async def send_vmix_command(
self,
device_id: str,
*,
assignment_id: str,
match_id: str,
command: dict[str, Any],
timeout: float = 5.0,
) -> dict[str, Any]:
async with self._device_vmix_send_lock(device_id):
return await self._send_vmix_command_unlocked(
device_id, assignment_id=assignment_id, match_id=match_id, command=command, timeout=timeout
)
async def _send_vmix_batch_unlocked(
self,
device_id: str,
*,
@@ -757,6 +781,20 @@ class VmixAgentHub:
if not pending_future.done():
pending_future.cancel()
async def send_vmix_batch(
self,
device_id: str,
*,
assignment_id: str,
match_id: str,
commands: list[dict[str, Any]],
timeout: float = 8.0,
) -> dict[str, Any]:
async with self._device_vmix_send_lock(device_id):
return await self._send_vmix_batch_unlocked(
device_id, assignment_id=assignment_id, match_id=match_id, commands=commands, timeout=timeout
)
async def test_set_text(
self,
device_id: str,
@@ -1286,12 +1324,13 @@ class VmixAgentHub:
raise HTTPException(status_code=404, detail="Agent не найден")
assignment_id = str(device.current_assignment_key or "")
match_id = str(device.current_match_external_id or "")
agent_version = str(device.agent_version or "")
if not assignment_id or not match_id:
raise HTTPException(status_code=409, detail="Agent не привязан к текущему матчу")
if operator_game_id and match_id != operator_game_id:
raise HTTPException(status_code=409, detail="Agent назначен на другой матч")
results: list[dict[str, Any]] = []
prepared_commands: list[dict[str, Any]] = []
for index, raw in enumerate(commands):
function = str(raw.get("Function") or raw.get("function") or "").strip()
if not function:
@@ -1303,36 +1342,62 @@ class VmixAgentHub:
if str(key).lower() != "value" and str(value) == "":
continue
command[str(key)] = value
ack = await self.send_vmix_command(
target_device_id,
assignment_id=assignment_id,
match_id=match_id,
command=command,
timeout=timeout,
)
item = {
"index": index,
"function": function,
"ok": bool(ack.get("ok")),
"reason": str(ack.get("reason") or ack.get("error") or ""),
}
results.append(item)
if not item["ok"]:
raise HTTPException(
status_code=502,
detail={
"message": f"vMix command failed: {function}",
"index": index,
"results": results,
},
prepared_commands.append(command)
results: list[dict[str, Any]] = []
transport = "batch" if self._agent_supports_batch(agent_version) and len(prepared_commands) > 1 else "legacy"
async with self._device_vmix_send_lock(target_device_id):
if transport == "batch":
ack = await self._send_vmix_batch_unlocked(
target_device_id,
assignment_id=assignment_id,
match_id=match_id,
commands=prepared_commands,
timeout=max(4.0, min(8.0, timeout + 2.0)),
)
self._track_runtime_overlay_command(
target_device_id,
command,
sequence_id=sequence_id,
sequence_name=sequence_name,
button_id=button_id,
)
ack_results = ack.get("results") if isinstance(ack.get("results"), list) else []
for index, command in enumerate(prepared_commands):
function = str(command.get("Function") or "")
item_ack = ack_results[index] if index < len(ack_results) and isinstance(ack_results[index], dict) else {}
ok = bool(item_ack.get("ok")) if item_ack else bool(ack.get("ok"))
item = {
"index": index,
"function": function,
"ok": ok,
"reason": str(item_ack.get("reason") or item_ack.get("error") or ("" if ok else ack.get("reason") or ack.get("error") or "vmix_batch_error")),
}
results.append(item)
if ok:
self._track_runtime_overlay_command(
target_device_id, command, sequence_id=sequence_id, sequence_name=sequence_name, button_id=button_id
)
if not all(item["ok"] for item in results):
failed = next(item for item in results if not item["ok"])
raise HTTPException(
status_code=502,
detail={"message": f"vMix command failed: {failed['function']}", "index": failed["index"], "results": results},
)
else:
for index, command in enumerate(prepared_commands):
function = str(command.get("Function") or "")
ack = await self._send_vmix_command_unlocked(
target_device_id, assignment_id=assignment_id, match_id=match_id, command=command, timeout=timeout
)
item = {
"index": index,
"function": function,
"ok": bool(ack.get("ok")),
"reason": str(ack.get("reason") or ack.get("error") or ""),
}
results.append(item)
if not item["ok"]:
raise HTTPException(
status_code=502,
detail={"message": f"vMix command failed: {function}", "index": index, "results": results},
)
self._track_runtime_overlay_command(
target_device_id, command, sequence_id=sequence_id, sequence_name=sequence_name, button_id=button_id
)
return {
"ok": True,
"device_id": target_device_id,
@@ -1341,6 +1406,7 @@ class VmixAgentHub:
"session_token": session_token,
"applied": len(results),
"results": results,
"transport": transport,
"overlay_state": self._runtime_overlay_payload(target_device_id),
}