Skip to content

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:

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));