Commit 8f79025
authored
fix: DCL Cast admin panel — auto-open, slide state, and reliable late-joiner replication (#1358)
* fix: auto-open the admin DCL Cast panel on presentation start and receive slide state
Two bugs in the admin toolkit's DCL Cast section:
1. The panel never auto-opened when a presentation started, for admins who were
not already sitting on the DCL Cast sub-tab. Presentation detection lived
inside the DclCast React component, so it only ran once the panel was already
open — a passive admin detected nothing.
2. Slide state (page count, current slide, video) never populated. The runtime's
`consumeMessages` resolves to a bare array of { sender, data }, but the code
destructured `const { messages }`, which yields undefined, throws on `.length`,
and is silently swallowed by the surrounding try/catch — dropping every reply.
Fixes (all scene-side; no explorer or cast-presenter-server changes needed):
- Move presentation detection into a boot-time, admin-gated background service
(presentation-detector.ts) that auto-opens the panel to Video Control when the
presentation bot's LiveKit track appears, independent of the active tab. Data
flows one way: detector -> state.videoControl.presentationState -> views.
- Read consumeMessages as an array (tolerating a { messages } wrapper too).
- ensurePresenterRole: parse { presenters: string[] } correctly and compare
addresses case-insensitively (it previously always re-promoted); sequence
getDclCastInfo -> ensurePresenterRole so the promotion targets an existing room.
- Extract showcaseState/sharePresentationState into a pure DclCast/state.ts;
add findActiveCastScreenIndex helper so auto-open selects the casting screen.
* docs: note that ~system/CommsApi consumeMessages returns a bare array
Records the gotcha behind this fix: the runtime returns [{ sender, data }], not
the { messages } wrapper the type implies, so `const { messages } = await
consumeMessages(...)` throws and is swallowed by the surrounding try/catch.
* fix: harden DCL Cast presentation detector per review
Address PR review feedback on the presentation detector's lifecycle and
error handling:
- Split the conflated `presentationSubscribed` flag into an edge tracker
(`presentationActive`, drives the one-shot auto-open) and a liveness flag
(`presentationSystem`, only assigned on successful setup). A failed setup
no longer wedges detection permanently — the next poll tick retries. (P1)
- Await `startPresentationSystem` inside the poll and wrap its setup in
try/catch/finally so it never leaves an unhandled rejection or a dangling
in-flight guard; bail if the presentation ended mid-setup. (P1)
- Add `stopPresentationDetection` — removes the poll interval and consume
systems and resets module state, giving the detector a real reset path. (P1)
- Log (console.error) instead of silently swallowing consume failures. (P1)
- Replace the unsafe double `as` casts in `consumePresentationMessages` with
a validating `extractMessages` helper that narrows each entry to a string
`data` before JSON.parse. (P2)
- Use `CAST_SRC_PREFIX` instead of the inline 'livekit-video://' literal, and
remove the leftover debug track-dump console.log. (P2)
- Retry button now calls `fetchDclCastInfo` so loading/error state updates. (P2)
* perf: tighten DCL Cast detection to 1s and throttle status consume to 250ms
- Detection poll 5s -> 1s so the panel auto-opens ~1s after a presentation
starts. The poll stays continuous by design (it's the authoritative
teardown/re-arm path and the only backstop for an ungraceful bot exit);
documented on the constant so it isn't "optimized" into a stop-on-detect.
- Status consume: drain the presentation comms topic every ~250ms (4Hz) via
the setInterval helper instead of every frame (~30Hz). Visually instant for
a slide counter/video state and the dominant cost, so net RPC during a
presentation drops ~6x even with the faster detection poll.
No logic change beyond cadence; each drain still returns the latest state, so
throttling never drops the final message (including presentation:stopped).
* refactor: make Admin Toolkit UI event-driven (single view-state owner + transitions)
Consolidate all panel/tab/sub-view state into a single store, mutated only through
named transitions, with the render a pure function of that state. This removes the
local-useState shadow and the imperative "blink" remount tricks that made the DCL Cast
auto-open unreliable, and gives the presentation lifecycle a deterministic path.
- New store.ts (single state owner) + actions.ts (named transitions); repointing the
state importers breaks the detector -> VideoControl/utils -> index import cycle.
- DCL Cast sub-view now reads state.videoControl.selectedTab, so a presentation
auto-opens the panel to Video Control -> DCL Cast regardless of remount or whether a
screen is casting yet.
- Presentation start -> showPresentation(); end (bot 'stopped' or track gone) ->
dismissPresentation() returns to the full DCL Cast panel. Manual Stop Sharing uses
the same end transition.
- Fold the showcase/share/moderation singletons and the announcement banner into the
store behind transitions; the announcement banner now resets on new input.
- Remove dead code: the unrendered RewardsControl tab and the now-unused nextTick
blink array + drain system.
* fix: reliably replicate cast stream to late joiners
A client joining after an admin enabled the cast intermittently showed the
default video (and, for admins, a stale "Activate" panel) instead of the cast.
The late-joiner handshake in the admin message bus was single-shot: the client
sent REQUEST_STATE once at init and applied the first SYNC_STATE reply. Both are
best-effort comms; if either was dropped the client never learned the trusted
src, and the per-frame revert system then pinned the screen to defaultURL even
though the cast src had already arrived over CRDT.
Retry REQUEST_STATE (1s interval, up to 10 attempts) until a remote SYNC_STATE
reply is applied, then stop. Keys strictly on receivedRemoteState (a full reply
from another participant) rather than adminHasActed, so a live SET_ANNOUNCEMENT
cannot stop the retry before the missing video state arrives. Only recovers
state that predates the join; live changes after joining still arrive via
SET_VIDEO.
* refactor: address review feedback on the Admin Toolkit event-driven refactor
Follow-up to the event-driven refactor tightening the "actions are the only
writers" invariant and hardening the presentation detector.
- Route the remaining direct state writes through actions: the video-player
dropdown (selectVideoPlayer), the stream toggles in DclCastInfo/ShowStreamKey
(setStream), the minimize/expand chevrons (minimizeCast/expandCast), and the
detector's dclCast/presentationState writes (setDclCastInfo / setPresentationState
/ clearPresentationState).
- Wire up the now-used minimize/expand transitions and drop the dead closePanel.
- Clear the stored modal handlers in closeShowcase/closeSharePresentation so their
captured render closures are not retained while the modal is hidden.
- Detector: add a poll reentrancy guard + top-level try/catch, remove the unused
stopPresentationDetection export and the vestigial injected state param (it is
now purely action-driven), and drop the discarded interval handle.
* chore: clear pre-existing lint in admin-toolkit-ui
Fix the 11 pre-existing ESLint errors in files the event-driven refactor did not
touch, so the directory lints clean end to end.
- consistent-type-imports: Active, Card, Loading, AddUserInput. Card keeps ReactEcs
as a value import (it is the JSX factory) with a targeted rule disable.
- no-unused-vars: drop the unused UiTransform import (Loading), add the JSX-factory
disable for ReactEcs (BanUserDescription), and remove three dead params from the
SmartItemsControl helper components.
* style: prettier-format the late-joiner REQUEST_STATE retry block
* refactor: address follow-up review on admin toolkit UI
- Complete the "actions are the only writers" migration: route the remaining
direct state writes in SmartItemsControl, TextAnnouncementsControl and the
UI-entity bootstrap through named actions, so the actions.ts invariant holds.
- Make unbanMessage `string | undefined` to match every other store optional.
- Drop the unnecessary export on handleGetDclCastInfo (used only in its module).
- Dedupe the presentation end callback so dismissPresentation fires once per
presentation (the comms 'stopped' message and the track-gone poll edge both
signal the end; latch resets on the start edge).
* fix: close review findings on the admin toolkit DCL Cast fixes
- P1: guard the consume .then() with presentationActive so a late-resolving
consumePresentationMessages can't repopulate stale slide state after the
falling-edge teardown cleared it (there was no self-healing path once the
consume system was stopped).
- ensurePresenterRole now returns whether the player is/became a presenter;
startPresentationSystem bails (leaving presentationSystem null) on failure so
the 1s poll retries, instead of running a consume loop that never receives
presenter-only state.
- setSmartItemAction drops the unsafe `as SelectedSmartItem` cast (guards on
presence); setSmartItemVisibility uses copy-on-write to match its siblings.
- receivedRemoteState only latches on a SYNC_STATE that carries video state, so
an announcement-only reply no longer stops the late-joiner video retry early.1 parent 0ee9d8f commit 8f79025
29 files changed
Lines changed: 778 additions & 629 deletions
File tree
- packages/asset-packs/src/admin-toolkit-ui
- ModerationControl
- VideoControl
- DclCast
- LiveStream
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
173 | 173 | | |
174 | 174 | | |
175 | 175 | | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
176 | 186 | | |
177 | 187 | | |
178 | 188 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
| 2 | + | |
| 3 | + | |
2 | 4 | | |
3 | 5 | | |
4 | 6 | | |
| |||
Lines changed: 2 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
5 | | - | |
| 4 | + | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| |||
Lines changed: 1 addition & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
1 | 2 | | |
2 | 3 | | |
3 | 4 | | |
| |||
Lines changed: 3 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
9 | 10 | | |
10 | | - | |
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| |||
89 | 89 | | |
90 | 90 | | |
91 | 91 | | |
92 | | - | |
| 92 | + | |
93 | 93 | | |
94 | 94 | | |
95 | 95 | | |
| |||
114 | 114 | | |
115 | 115 | | |
116 | 116 | | |
117 | | - | |
| 117 | + | |
118 | 118 | | |
119 | 119 | | |
120 | 120 | | |
| |||
Lines changed: 16 additions & 13 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
| 9 | + | |
8 | 10 | | |
9 | 11 | | |
10 | 12 | | |
11 | | - | |
| 13 | + | |
12 | 14 | | |
13 | 15 | | |
14 | 16 | | |
| |||
74 | 76 | | |
75 | 77 | | |
76 | 78 | | |
77 | | - | |
| 79 | + | |
78 | 80 | | |
79 | | - | |
| 81 | + | |
80 | 82 | | |
81 | 83 | | |
82 | 84 | | |
| |||
88 | 90 | | |
89 | 91 | | |
90 | 92 | | |
91 | | - | |
| 93 | + | |
92 | 94 | | |
93 | 95 | | |
94 | 96 | | |
95 | 97 | | |
96 | 98 | | |
97 | 99 | | |
98 | | - | |
| 100 | + | |
99 | 101 | | |
100 | | - | |
| 102 | + | |
101 | 103 | | |
102 | 104 | | |
103 | 105 | | |
104 | 106 | | |
105 | 107 | | |
106 | | - | |
| 108 | + | |
107 | 109 | | |
108 | 110 | | |
109 | 111 | | |
110 | 112 | | |
111 | 113 | | |
112 | 114 | | |
113 | | - | |
| 115 | + | |
114 | 116 | | |
115 | 117 | | |
116 | 118 | | |
117 | 119 | | |
118 | 120 | | |
119 | 121 | | |
120 | 122 | | |
121 | | - | |
| 123 | + | |
122 | 124 | | |
123 | | - | |
| 125 | + | |
| 126 | + | |
124 | 127 | | |
125 | 128 | | |
126 | | - | |
| 129 | + | |
127 | 130 | | |
128 | 131 | | |
129 | 132 | | |
| |||
288 | 291 | | |
289 | 292 | | |
290 | 293 | | |
291 | | - | |
| 294 | + | |
292 | 295 | | |
293 | 296 | | |
294 | | - | |
| 297 | + | |
295 | 298 | | |
296 | 299 | | |
297 | 300 | | |
| |||
Lines changed: 3 additions & 15 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| 11 | + | |
11 | 12 | | |
12 | 13 | | |
13 | 14 | | |
| |||
42 | 43 | | |
43 | 44 | | |
44 | 45 | | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | | - | |
53 | | - | |
54 | | - | |
55 | | - | |
56 | | - | |
57 | | - | |
58 | 46 | | |
59 | 47 | | |
60 | 48 | | |
| |||
79 | 67 | | |
80 | 68 | | |
81 | 69 | | |
82 | | - | |
| 70 | + | |
83 | 71 | | |
84 | 72 | | |
85 | 73 | | |
| |||
97 | 85 | | |
98 | 86 | | |
99 | 87 | | |
100 | | - | |
| 88 | + | |
101 | 89 | | |
102 | 90 | | |
103 | 91 | | |
| |||
0 commit comments