feat: overhaul video player controller
This commit is contained in:
+110
-33
@@ -18,6 +18,7 @@ from .store import ControllerStore
|
||||
TERMINAL_REASONS = frozenset(
|
||||
{"stop", "finished", "mpv_error", "sigterm", "sigint", "shutdown"}
|
||||
)
|
||||
TERMINAL_ERROR_REASONS = frozenset({"startup_error", "worker_start_error"})
|
||||
CONTROL_ACTIONS = frozenset({"play", "stop", "pause", "resume", "toggle"})
|
||||
|
||||
|
||||
@@ -485,13 +486,31 @@ class PlaybackReducer:
|
||||
return effects
|
||||
|
||||
if state == "busy":
|
||||
if slot is not None or path is not None:
|
||||
previous = self.store.player_observation()
|
||||
if (
|
||||
slot is not None
|
||||
or path is not None
|
||||
or previous.active_slot is not None
|
||||
or previous.active_path is not None
|
||||
):
|
||||
self._active_evidence(
|
||||
slot=slot,
|
||||
path=path,
|
||||
paused=None,
|
||||
status=payload,
|
||||
)
|
||||
else:
|
||||
self.store.set_player_observation(
|
||||
availability="online",
|
||||
phase="unknown",
|
||||
active_slot=None,
|
||||
active_path=None,
|
||||
paused=None,
|
||||
reconciliation_state="unknown",
|
||||
reconciliation_due_at=None,
|
||||
reconciliation_id=None,
|
||||
status_payload=payload,
|
||||
)
|
||||
effects.append(PlaybackEffect("player_busy", slot=slot, message=payload.get("message")))
|
||||
if self.pending is not None and self.pending.action == "play":
|
||||
effects.append(self._finish_operation("failed", "Player is busy"))
|
||||
@@ -499,40 +518,87 @@ class PlaybackReducer:
|
||||
|
||||
if state == "error":
|
||||
previous = self.store.player_observation()
|
||||
message = str(payload.get("message") or "Player error")
|
||||
reason = payload.get("reason")
|
||||
if reason in TERMINAL_ERROR_REASONS:
|
||||
terminal_slot = slot if slot is not None else previous.active_slot
|
||||
self._idle_evidence(status=payload)
|
||||
effects.extend(
|
||||
(
|
||||
PlaybackEffect(
|
||||
"player_error",
|
||||
slot=terminal_slot,
|
||||
message=message,
|
||||
),
|
||||
PlaybackEffect(
|
||||
"player_idle",
|
||||
slot=terminal_slot,
|
||||
message=str(reason),
|
||||
),
|
||||
)
|
||||
)
|
||||
if self.pending is not None:
|
||||
effects.append(self._finish_operation("failed", message))
|
||||
return effects
|
||||
|
||||
active_slot = slot if slot is not None else previous.active_slot
|
||||
active_path = path if path is not None else previous.active_path
|
||||
due = None
|
||||
if self.config.storage.uncertain_review_after_seconds > 0:
|
||||
due = (
|
||||
self._wall_clock()
|
||||
+ self.config.storage.uncertain_review_after_seconds
|
||||
)
|
||||
if active_slot is not None:
|
||||
self.store.reconcile_active_guard(
|
||||
guard = self.store.mark_slot_uncertain(
|
||||
active_slot,
|
||||
kind="player_active",
|
||||
details={"error": payload.get("message")},
|
||||
review_after=due,
|
||||
details={"reason": "Player error", "message": message},
|
||||
)
|
||||
due = guard.review_after
|
||||
self.store.set_player_observation(
|
||||
availability="online",
|
||||
phase="error",
|
||||
active_slot=active_slot,
|
||||
active_path=active_path,
|
||||
paused=(
|
||||
previous.paused
|
||||
if self._same_playback_identity(
|
||||
previous,
|
||||
active_slot,
|
||||
active_path,
|
||||
)
|
||||
else None
|
||||
),
|
||||
reconciliation_state=(
|
||||
"operator_review"
|
||||
if guard.state == "review_required"
|
||||
else "awaiting_evidence"
|
||||
),
|
||||
reconciliation_due_at=due,
|
||||
reconciliation_id=guard.guard_id,
|
||||
status_payload=payload,
|
||||
)
|
||||
elif active_path is not None:
|
||||
self.store.reconcile_external_active()
|
||||
self.store.set_player_observation(
|
||||
availability="online",
|
||||
phase="error",
|
||||
active_slot=active_slot,
|
||||
active_path=active_path,
|
||||
paused=(
|
||||
previous.paused
|
||||
if self._same_playback_identity(
|
||||
previous,
|
||||
active_slot,
|
||||
active_path,
|
||||
)
|
||||
else None
|
||||
),
|
||||
reconciliation_state=(
|
||||
"confirmed"
|
||||
if active_slot is not None or active_path is not None
|
||||
else "unknown"
|
||||
),
|
||||
reconciliation_due_at=None,
|
||||
reconciliation_id=None,
|
||||
status_payload=payload,
|
||||
)
|
||||
message = str(payload.get("message") or "Player error")
|
||||
self.store.mark_external_uncertain(
|
||||
active_path,
|
||||
review_after=due,
|
||||
availability="online",
|
||||
details={"reason": "Player error", "message": message},
|
||||
status_payload=payload,
|
||||
)
|
||||
else:
|
||||
self.store.set_player_observation(
|
||||
availability="online",
|
||||
phase="unknown",
|
||||
active_slot=None,
|
||||
active_path=None,
|
||||
paused=None,
|
||||
reconciliation_state="unknown",
|
||||
reconciliation_due_at=None,
|
||||
reconciliation_id=None,
|
||||
status_payload=payload,
|
||||
)
|
||||
effects.append(PlaybackEffect("player_error", slot=active_slot, message=message))
|
||||
if self.pending is not None:
|
||||
effects.append(self._finish_operation("failed", message))
|
||||
@@ -645,6 +711,19 @@ class PlaybackReducer:
|
||||
"reason": "stop timeout",
|
||||
},
|
||||
)
|
||||
elif operation.action in {"pause", "resume", "toggle"}:
|
||||
previous = self.store.player_observation()
|
||||
if previous.active_slot is not None or previous.active_path is not None:
|
||||
self.store.set_player_observation(
|
||||
availability=previous.availability,
|
||||
phase="unknown",
|
||||
active_slot=previous.active_slot,
|
||||
active_path=previous.active_path,
|
||||
paused=None,
|
||||
reconciliation_state=previous.reconciliation_state,
|
||||
reconciliation_due_at=previous.reconciliation_due_at,
|
||||
reconciliation_id=previous.reconciliation_id,
|
||||
)
|
||||
return self._finish_operation("timed_out", "no authoritative Player confirmation")
|
||||
|
||||
def _mark_play_uncertain(self, operation: PlayerOperation, reason: str) -> None:
|
||||
@@ -753,8 +832,6 @@ class PlaybackReducer:
|
||||
|
||||
def projected_slot_state(self, slot: int) -> str:
|
||||
record = self.store.get_slot(slot)
|
||||
if record.media_state != "ready":
|
||||
return record.media_state
|
||||
observation = self.store.player_observation()
|
||||
if observation.active_slot == slot:
|
||||
if observation.reconciliation_state == "operator_review":
|
||||
@@ -771,9 +848,9 @@ class PlaybackReducer:
|
||||
)
|
||||
if guard is not None:
|
||||
if guard.kind == "pending_play":
|
||||
return "ready"
|
||||
return record.media_state
|
||||
return "operator_review" if guard.state == "review_required" else "unknown"
|
||||
return "ready"
|
||||
return record.media_state
|
||||
|
||||
def mark_due_reconciliation(self) -> list[PlaybackEffect]:
|
||||
now = self._wall_clock()
|
||||
|
||||
Reference in New Issue
Block a user