поправлены способ передачи таймеров в vMix
This commit is contained in:
@@ -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),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user