Appearance
Activity
Overlays are shared, exclusive on-screen slots. You reserve one with an activity request, then animate. This works both as a reaction to a payment and for a free, model-triggered effect. In the snippets, ext is your createExtHelper() instance.
Overlays overview
There are two overlay slots, both a shared and exclusive resource:
EXTENSION_SLOT_RIGHT_OVERLAY— a panel beside the video.EXTENSION_SLOT_VIDEO_DECORATIVE_OVERLAY— sits over the video and receives no pointer events.
Because they are shared, always reserve the slot with an activity request before you animate.
Play an overlay reaction to an action
Broadcast the result to the whole room; every client plays the overlay. The paying viewer and the model use the same broadcast — the only difference is the paymentHash.
ts
// background slot
// Viewer (paid): broadcast from the payment's succeeded handler.
// In a public show the broadcast must carry the paymentHash.
ext.subscribe('v1.payment.tokens.spend.succeeded', async ({ paymentHash, tokensSpendData }) => {
if (tokensSpendData.type !== 'SPIN') return;
await ext.makeRequest('v1.ext.whisper', {
paymentHash,
data: { type: 'SPIN_RESULT', result: yourComputeResult() },
});
});
// Model (free): broadcast the same event WITHOUT a paymentHash.
async function yourModelSpin() {
await ext.makeRequest('v1.ext.whisper', {
data: { type: 'SPIN_RESULT', result: yourComputeResult() },
});
}ts
// every client plays the overlay
ext.subscribe('v1.ext.whispered', async ({ type, result }) => {
if (type !== 'SPIN_RESULT') return;
await yourPlayOverlay(result);
});Reserve the overlay before animating
Reserve the slot with v1.ext.activity.request and only start the animation after it resolves. The lease must be longer than the animation.
ts
const SPIN_MS = 8000;
const HOLD_MS = 4000;
const SLACK_MS = 1000; // safety margin so the slot never closes mid-animation
const LEASE_MS = SPIN_MS + HOLD_MS + SLACK_MS; // always longer than the animation
async function yourPlayOverlay(result) {
// waits if another extension holds the slot, then starts the timer
await ext.makeRequest('v1.ext.activity.request', {
durationMs: LEASE_MS, // max 30000
});
await yourRunAnimation(result, SPIN_MS); // start ONLY after the request resolves
await yourHoldWinner(HOLD_MS);
// the slot frees itself when durationMs elapses
}WARNING
durationMs must be greater than the animation length, or the slot closes mid-animation. The request may queue behind another extension, so never start the animation before it resolves.
Show a decorative overlay over the video
For non-interactive effects on top of the stream. The slot is visible only while an activity is held.
ts
// background slot: on a meaningful event, reserve the slot and tell the overlay to render
ext.subscribe('v1.ext.whispered', async ({ type, payload }) => {
if (type !== 'CELEBRATE') return;
await ext.makeRequest('v1.ext.activity.request', { durationMs: 6000 });
ext.makeRequest('v1.ext.whisper.local', { data: { type: 'PLAY_FX', payload } });
});ts
// videoDecorativeOverlay: render only — this slot receives no pointer events
ext.subscribe('v1.ext.whispered.local', ({ type, payload }) => {
if (type === 'PLAY_FX') yourRenderEffect(payload);
});Recommendations: disable controls while the overlay is busy
Prevent a second trigger while an animation is playing (yours or another viewer's).
ts
const { status } = await ext.makeRequest('v1.ext.activity.status', null);
yourSetDisabled(status === 'busy');
ext.subscribe('v1.ext.activity.busy', () => yourSetDisabled(true));
ext.subscribe('v1.ext.activity.available', () => yourSetDisabled(false));