Add sync clock entity for synchronized animation timing

Introduces Synchronization Clocks — shared, controllable time bases
that CSS sources can optionally reference for synchronized animation.

Backend:
- New SyncClock dataclass, JSON store, Pydantic schemas, REST API
- Runtime clock with thread-safe pause/resume/reset and speed control
- Ref-counted runtime pool with eager creation for API control
- clock_id field on all ColorStripSource types
- Stream integration: clock time/speed replaces source-local values
- Paused clock skips rendering (saves CPU + stops frame pushes)
- Included in backup/restore via STORE_MAP

Frontend:
- Sync Clocks tab in Streams section with cards and controls
- Clock dropdown in CSS editor (hidden speed slider when clock set)
- Clock crosslink badge on CSS source cards (replaces speed badge)
- Targets tab uses DataCache for picture/audio sources and sync clocks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 21:46:55 +03:00
parent 52ee4bdeb6
commit aa1e4a6afc
32 changed files with 1255 additions and 58 deletions

View File

@@ -0,0 +1,43 @@
"""Synchronization clock data model.
A SyncClock provides a shared, controllable time base for animation-based
color strip sources. Multiple CSS sources referencing the same clock
animate in sync and share speed / pause / resume / reset controls.
"""
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
@dataclass
class SyncClock:
"""Persistent configuration for a synchronization clock."""
id: str
name: str
speed: float # animation speed multiplier (0.110.0)
created_at: datetime
updated_at: datetime
description: Optional[str] = None
def to_dict(self) -> dict:
return {
"id": self.id,
"name": self.name,
"speed": self.speed,
"description": self.description,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
}
@staticmethod
def from_dict(data: dict) -> "SyncClock":
return SyncClock(
id=data["id"],
name=data["name"],
speed=float(data.get("speed", 1.0)),
description=data.get("description"),
created_at=datetime.fromisoformat(data["created_at"]),
updated_at=datetime.fromisoformat(data["updated_at"]),
)