362e089be0
Der Nutzer hat recht: Der Ordner war voller Entwicklungs-Muell. Jetzt ist sauber getrennt: ROOT (was der Nutzer sieht und braucht): - run.py = das Programm - hms_app/ = der Anwendungscode - HMS MediaEngine.app = macOS Doppelklick-Starter - HMS-Start.vbs = Windows Doppelklick-Starter - HMS-Install.vbs = Windows Erst-Installation - HMS-Mac-Install.command = macOS Homebrew-Installation - HMS-Portable-Install.command = macOS Portable-Installation (16GB-Fix) - installer_gui.py = grafischer Installer - launcher.pyw + launcher_core.py = interne Start-Logik - LIESMICH.txt = 10-Zeilen-Kurzanleitung - .gitignore _entwicklung/ (alles andere, NICHT benoetigt): - packages/ apps/ native/ plugins/ tools/ schemas/ tests/ docs/ build/ fixture_profiles/ - PLAN.md STATUS.md ERRORS.md TEST_REPORT.md CHANGELOG.md README.md - pyproject.toml uv.lock setup_*.sh/ps1 make_mac_app.py Diese Trennung gilt ab sofort fuer alle Commits. Der Nutzer kann _entwicklung/ loeschen wenn er Platz braucht - die App laeuft ohne. Verifiziert: App startet nach Aufraeumen unveraendert (Health 200).
269 lines
9.6 KiB
Python
269 lines
9.6 KiB
Python
"""Renderer-Engine: verbindet StateMirror, Playback und Preload (§12, §6.4).
|
|
|
|
Der Rendergraph läuft in nativem GStreamer/Rust (ADR-0004); diese Engine
|
|
orchestriert:
|
|
- liest Showparameter aus dem RemoteStateMirror (§6.4)
|
|
- berechnet Playback-Positionen je Quelle (§12.5)
|
|
- verwaltet Preload-Slots für atomaren Clipwechsel (§12.2)
|
|
- bildet pro Frame einen unveränderlichen Parameter-Snapshot (§11.4)
|
|
- erzeugt Rendertelemetrie (§28.2)
|
|
|
|
Keine Pixelverarbeitung in Python (§33); die GPU-Pipeline wird über
|
|
Pipeline-Definitionen an GStreamer übergeben.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from dataclasses import dataclass
|
|
|
|
from hms_domain.model import TransportState
|
|
from hms_media import PlaybackController, PreloadSlot
|
|
|
|
from hms_renderer.state_mirror import RemoteStateMirror
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FrameSnapshot:
|
|
"""Unveränderlicher Parameter-Snapshot für genau einen Frame (§11.4).
|
|
|
|
Der Rendergraph (Rust/GStreamer) erhält genau dieses Objekt pro Frame;
|
|
halbe Zustände sind ausgeschlossen.
|
|
"""
|
|
|
|
frame_index: int
|
|
monotonic_ns: int
|
|
state_revision: int
|
|
parameters: dict[str, float]
|
|
source_positions: dict[str, float] # source_id → normalisierte Position
|
|
source_states: dict[str, str] # source_id → TransportState
|
|
active_asset_ids: dict[str, str | None] # layer_key → asset_id (nach Commit)
|
|
|
|
|
|
@dataclass
|
|
class RenderTelemetry:
|
|
"""Rendertelemetrie je Frame (§28.2, §25.4)."""
|
|
|
|
frame_index: int = 0
|
|
fps: float = 0.0
|
|
frame_time_ms: float = 0.0
|
|
dropped_frames: int = 0
|
|
active_layers: int = 0
|
|
preload_pending: int = 0
|
|
source_events: int = 0
|
|
|
|
|
|
class SourceHandle:
|
|
"""Verwaltet eine Medienquelle im Renderer (§12.5, §12.2).
|
|
|
|
Verbindet PlaybackController mit Parameterpfaden und einem PreloadSlot:
|
|
- Transport-Befehle aus dem StateMirror steuern den Controller
|
|
- Der PreloadSlot wechselt Clips atomar bei Commit
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
source_id: str,
|
|
parameter_prefix: str,
|
|
fps: float = 60.0,
|
|
) -> None:
|
|
self.source_id = source_id
|
|
self.parameter_prefix = parameter_prefix
|
|
self.controller = PlaybackController(source_id=source_id)
|
|
self.preload = PreloadSlot()
|
|
self._active_asset_id: str | None = None
|
|
self._fps = fps
|
|
self._last_events: list = []
|
|
|
|
@property
|
|
def active_asset_id(self) -> str | None:
|
|
return self._active_asset_id
|
|
|
|
@property
|
|
def position(self) -> float:
|
|
return self.controller.position
|
|
|
|
@property
|
|
def state(self) -> TransportState:
|
|
return self.controller.state
|
|
|
|
def sync_from_mirror(self, mirror: RemoteStateMirror) -> list:
|
|
"""Liest Transport-Befehle und Parameter aus dem StateMirror.
|
|
|
|
Rückgabe: PlaybackEvents dieses Sync-Schritts (§12.5).
|
|
"""
|
|
events: list = []
|
|
|
|
# Transport-State aus dem Mirror lesen (§11.2)
|
|
state_value = mirror.get_value(f"{self.parameter_prefix}/source/state")
|
|
if state_value is not None:
|
|
state_map = {
|
|
0: TransportState.STOPPED,
|
|
1: TransportState.PLAYING,
|
|
2: TransportState.PAUSED,
|
|
}
|
|
target_state = state_map.get(int(state_value))
|
|
if target_state is not None:
|
|
is_playing = target_state is TransportState.PLAYING
|
|
is_paused = target_state is TransportState.PAUSED
|
|
is_stopped = target_state is TransportState.STOPPED
|
|
if is_playing and self.controller.state is not TransportState.PLAYING:
|
|
self.controller.play()
|
|
elif is_paused and self.controller.state is TransportState.PLAYING:
|
|
self.controller.pause()
|
|
elif is_stopped and self.controller.state is not TransportState.STOPPED:
|
|
self.controller.stop()
|
|
|
|
# Playback-Parameter aus dem Mirror (§10.2)
|
|
speed = mirror.get_value(f"{self.parameter_prefix}/source/speed")
|
|
if speed is not None and speed != 0:
|
|
self.controller.speed = max(-4.0, min(4.0, speed))
|
|
|
|
in_point = mirror.get_value(f"{self.parameter_prefix}/source/in_point")
|
|
if in_point is not None:
|
|
self.controller.in_point = max(0.0, min(0.99, in_point))
|
|
|
|
out_point = mirror.get_value(f"{self.parameter_prefix}/source/out_point")
|
|
if out_point is not None:
|
|
self.controller.out_point = max(self.controller.in_point + 0.01, min(1.0, out_point))
|
|
|
|
# Retrigger: Flankenwert im Mirror (1.0 = triggern, danach zurück auf 0)
|
|
retrigger = mirror.get_value(f"{self.parameter_prefix}/source/retrigger")
|
|
if retrigger is not None and retrigger > 0.5:
|
|
self.controller.retrigger()
|
|
|
|
# Clip-Auswahl über PreloadSlot (§12.2, §16.5 Load/Commit)
|
|
pending_asset = mirror.get_value(f"{self.parameter_prefix}/source/asset_id_pending")
|
|
if pending_asset is not None and pending_asset > 0:
|
|
# Asset-ID ist als Hash/Integer im Mirror; real: UUID-String aus Registry
|
|
# Hier: Asset-Wechsel nur über Commit (PreloadSlot)
|
|
self.preload.preload(str(int(pending_asset)))
|
|
|
|
commit = mirror.get_value(f"{self.parameter_prefix}/source/commit")
|
|
if commit is not None and commit > 0.5:
|
|
self.preload.mark_ready(str(int(mirror.get_value(
|
|
f"{self.parameter_prefix}/source/asset_id_pending", 0
|
|
))))
|
|
committed = self.preload.commit()
|
|
if committed:
|
|
self._active_asset_id = committed # atomar gewechselt (§12.2)
|
|
|
|
self._last_events = events
|
|
return events
|
|
|
|
def advance(self, dt_s: float, now_ns: int) -> list:
|
|
"""Advancement des PlaybackControllers; liefert Events."""
|
|
return self.controller.advance(dt_s, now_ns)
|
|
|
|
|
|
class RenderEngine:
|
|
"""Zentrale Renderer-Engine: orchestriert alle Quellen pro Frame.
|
|
|
|
Ablauf pro Frame (§12.2: feste Master-Bildrate):
|
|
1. sync_from_mirror: Transport-Parameter aus dem StateMirror lesen
|
|
2. advance: Playback-Positionen um dt weiterschieben
|
|
3. snapshot: unveränderlichen Frame-Snapshot bilden
|
|
4. telemetry: Frame-Statistiken aktualisieren
|
|
"""
|
|
|
|
def __init__(self, mirror: RemoteStateMirror, fps: float = 60.0) -> None:
|
|
self._mirror = mirror
|
|
self._fps = fps
|
|
self._frame_duration_s = 1.0 / fps
|
|
self._sources: dict[str, SourceHandle] = {}
|
|
self._frame_index = 0
|
|
self._last_frame_ns = 0
|
|
self.telemetry = RenderTelemetry()
|
|
|
|
@property
|
|
def mirror(self) -> RemoteStateMirror:
|
|
return self._mirror
|
|
|
|
@property
|
|
def fps(self) -> float:
|
|
return self._fps
|
|
|
|
@property
|
|
def frame_index(self) -> int:
|
|
return self._frame_index
|
|
|
|
def add_source(self, source_id: str, parameter_prefix: str) -> SourceHandle:
|
|
"""Registriert eine Medienquelle im Renderer."""
|
|
handle = SourceHandle(source_id, parameter_prefix, self._fps)
|
|
self._sources[source_id] = handle
|
|
return handle
|
|
|
|
def remove_source(self, source_id: str) -> None:
|
|
self._sources.pop(source_id, None)
|
|
|
|
def get_source(self, source_id: str) -> SourceHandle | None:
|
|
return self._sources.get(source_id)
|
|
|
|
def sources(self) -> list[SourceHandle]:
|
|
return list(self._sources.values())
|
|
|
|
def tick(self, now_ns: int | None = None) -> FrameSnapshot:
|
|
"""Ein Frame: Sync → Advance → Snapshot.
|
|
|
|
Wird vom Renderer-Loop mit fester Master-Bildrate aufgerufen (§12.2).
|
|
"""
|
|
now = now_ns if now_ns is not None else time.monotonic_ns()
|
|
if self._last_frame_ns == 0:
|
|
self._last_frame_ns = now
|
|
dt_s = (now - self._last_frame_ns) / 1e9
|
|
self._last_frame_ns = now
|
|
|
|
# 1. Sync: Parameter aus dem Mirror lesen
|
|
all_events: list = []
|
|
for handle in self._sources.values():
|
|
handle.sync_from_mirror(self._mirror)
|
|
|
|
# 2. Advance: Playback weiterschieben
|
|
for handle in self._sources.values():
|
|
events = handle.advance(dt_s, now)
|
|
all_events.extend(events)
|
|
|
|
# 3. Snapshot: unveränderlicher Frame-Zustand (§11.4)
|
|
self._frame_index += 1
|
|
parameters = self._mirror.all_values()
|
|
source_positions: dict[str, float] = {}
|
|
source_states: dict[str, str] = {}
|
|
active_assets: dict[str, str | None] = {}
|
|
|
|
for source_id, handle in self._sources.items():
|
|
source_positions[source_id] = handle.position
|
|
source_states[source_id] = handle.state.value
|
|
active_assets[f"{handle.parameter_prefix}/source"] = handle.active_asset_id
|
|
|
|
snapshot = FrameSnapshot(
|
|
frame_index=self._frame_index,
|
|
monotonic_ns=now,
|
|
state_revision=self._mirror.revision,
|
|
parameters=parameters,
|
|
source_positions=source_positions,
|
|
source_states=source_states,
|
|
active_asset_ids=active_assets,
|
|
)
|
|
|
|
# 4. Telemetry (§28.2)
|
|
frame_time_ms = dt_s * 1000.0
|
|
active_count = sum(
|
|
1 for h in self._sources.values()
|
|
if h.state is TransportState.PLAYING
|
|
)
|
|
preload_count = sum(
|
|
1 for h in self._sources.values()
|
|
if h.preload.pending is not None
|
|
)
|
|
self.telemetry = RenderTelemetry(
|
|
frame_index=self._frame_index,
|
|
fps=1000.0 / max(frame_time_ms, 0.01),
|
|
frame_time_ms=frame_time_ms,
|
|
dropped_frames=0, # zählt der native Rendergraph
|
|
active_layers=active_count,
|
|
preload_pending=preload_count,
|
|
source_events=len(all_events),
|
|
)
|
|
|
|
return snapshot
|