Clips Player

KICK TWITCH

Design a custom clip player for either platform using Meld's built-in AI.

1 Copy the Prompt

This block contains everything Meld's AI needs. The AI will ask which platform you stream on, then your channel name, before building the widget. One prompt, either platform.

I want to build a custom clips player overlay for my stream. The player pulls top clips from either Kick or Twitch and plays them back-to-back. A data layer script is included at the bottom of this message — it handles all the clip fetching, URL resolution, and preloading. Your job is to build the UI on top of it.

## STEP ZERO — CRITICAL, DO THIS FIRST

Before ANYTHING else — before acknowledging the task, before describing what you'll build, before asking about style — your FIRST message to me must be exactly this single question and nothing else:

  "Which platform? (Kick or Twitch)"

Wait for my answer. Once I answer Kick or Twitch, ask ONE more question and nothing else:

  "What's your <platform> channel name?"

A real channel name is a non-empty string of letters, numbers, or underscores (no spaces, no URLs, no "skip", no "idk"). If I try to skip or answer something unrelated, re-ask until I give a real name.

Once I answer, confirm both back to me:
  "Got it — building a <platform> clips player for `<channel>`."

THEN proceed to the styling questions. You must also:
  - In the data layer, replace `PLATFORM_PLACEHOLDER` with `"kick"` or `"twitch"` (lowercase, quoted).
  - Replace `CHANNEL_PLACEHOLDER` with the channel name I gave you (quoted).

If you generate the final widget with either placeholder string still in it, that is a failure — re-generate it.

## How the data layer works

It exposes a global `window.clipsPlayer` object (identical API regardless of platform):

  window.clipsPlayer.ready          — boolean, true once clips are loaded
  window.clipsPlayer.platform       — "kick" or "twitch"
  window.clipsPlayer.channel        — the channel name
  window.clipsPlayer.clips          — array of clip objects (shape below)
  window.clipsPlayer.currentIndex   — index of the currently playing clip
  window.clipsPlayer.currentClip()  — returns the currently playing clip object

Each clip has this shape (fields with no equivalent on a given platform are empty strings or 0):

  {
    id:              "unique clip id",
    title:           "clip title",
    creatorUsername: "person who made the clip",
    creatorAvatar:   "avatar URL or empty",
    viewCount:       12345,
    durationMs:      28000
  }

It fires these events on window:

  "clips:ready"       — clips are loaded, first clip starts playing
  "clips:clipchange"  — a new clip started. e.detail = { clip, index }
  "clips:ended"       — a clip finished (fires right before clips:clipchange for the next one)

To control playback:

  window.clipsPlayer.next()          — skip to next clip
  window.clipsPlayer.prev()          — go to previous clip
  window.clipsPlayer.pause()
  window.clipsPlayer.play()
  window.clipsPlayer.setVolume(0..1)

The data layer includes a hidden `<video id="clips-video-player">` element that's already playing the clip. Your overlay renders ON TOP of it — title cards, creator attribution, progress bars, transition effects, whatever. DO NOT create your own video element.

## After Step Zero — ask me these styling questions, ONE OR TWO at a time

  1. What's the overall vibe? (examples: minimal/invisible, retro TV/VHS, cinematic with film grain, news ticker, arcade, vaporwave, cyberpunk HUD, esports broadcast, late-night talk show)
  2. What info should overlay on the clip?
     - creator username + avatar
     - clip title
     - view count
     - "clip X of Y" counter
     - nothing (just the clip plays clean)
  3. Where should the overlay sit? (bottom bar, top ribbon, corner card, full-frame cinema bars, fade-on-start only)
  4. Transitions between clips — hard cut, crossfade, glitch, VHS static, swipe, film-projector-style?
  5. Should the overlay be always visible, or fade in at clip start and fade out after a few seconds?
  6. Any specific fonts? (Google Fonts are fine.)
  7. Any branding elements — logo, color palette, background music bed?

## Rules for the final widget

  - DO NOT re-implement the clip fetching. Use `window.clipsPlayer` only.
  - DO NOT create a second `<video>` element. Style/overlay the existing one.
  - Re-render overlay content on the "clips:clipchange" event.
  - Background must be transparent outside the video — this runs as an OBS/Meld browser source.
  - Handle long titles and creator names gracefully (ellipsis or marquee scroll).
  - Prefer vanilla JS and CSS. No frameworks unless I ask.
  - Include the data layer script below VERBATIM (with PLATFORM_PLACEHOLDER and CHANNEL_PLACEHOLDER replaced). Do not modify anything else.

## Data layer (include verbatim after placeholder substitution)

```html
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<video id="clips-video-player" width="1920" height="1080" autoplay playsinline style="width:100%;height:100%;object-fit:contain;background:transparent;"></video>
<video id="clips-preload-player" muted playsinline style="display:none;"></video>
<script>
(function() {
  const PLATFORM = PLATFORM_PLACEHOLDER; // ← "kick" or "twitch"
  const CHANNEL  = CHANNEL_PLACEHOLDER;  // ← channel name
  const TWITCH_ENDPOINT = "https://mrboostlive.com/twitch/clips/channel.php"; // server-side Twitch bridge

  const urlParams = new URLSearchParams(window.location.search);
  const volume = parseFloat(urlParams.get('volume') || '50') / 100;

  window.clipsPlayer = {
    ready: false,
    platform: PLATFORM,
    channel: CHANNEL,
    clips: [],
    currentIndex: 0,
    currentClip() { return this.clips[this.currentIndex] || null; }
  };

  // ---- Kick fetcher (direct client-side) ----
  async function fetchKickClips(cursor = 0) {
    const base = `https://kick.com/api/v2/channels/${CHANNEL}/clips?sort=view&time=all`;
    const res = await fetch(`${base}&cursor=${cursor}`);
    if (!res.ok) throw new Error(`Kick HTTP ${res.status}`);
    const data = await res.json();
    const batch = (data.clips || []).map(c => ({
      id: c.id,
      url: c.video_url,
      title: c.title || '',
      creatorUsername: c.creator?.username || '',
      creatorAvatar: c.creator?.profile_picture || '',
      viewCount: c.views || 0,
      durationMs: (c.duration || 0) * 1000
    }));
    if (data.nextCursor) batch.push(...await fetchKickClips(data.nextCursor));
    return batch;
  }

  // ---- Twitch fetcher (proxied through PHP bridge) ----
  // Twitch clip URLs require a signed token per playback, so the PHP page handles
  // the clip list. We fetch the page's clip metadata via a companion JSON endpoint.
  async function fetchTwitchClips() {
    const res = await fetch(`${TWITCH_ENDPOINT}?channel=${encodeURIComponent(CHANNEL)}&format=json`);
    if (!res.ok) throw new Error(`Twitch bridge HTTP ${res.status}`);
    const data = await res.json();
    return (data.clips || []).map(c => ({
      id: c.id,
      url: c.url || '',           // signed playback URL (already resolved by the bridge)
      title: c.title || '',
      creatorUsername: c.creator_name || '',
      creatorAvatar: c.creator_avatar || '',
      viewCount: c.view_count || 0,
      durationMs: (c.duration || 0) * 1000
    }));
  }

  function shuffle(arr) {
    for (let i = arr.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [arr[i], arr[j]] = [arr[j], arr[i]];
    }
    return arr;
  }

  const mainPlayer = document.getElementById('clips-video-player');
  const preloadPlayer = document.getElementById('clips-preload-player');
  mainPlayer.volume = volume;

  function loadInto(player, src) {
    if (!src) return;
    if (src.endsWith('.m3u8')) {
      if (window.Hls && Hls.isSupported()) {
        if (player._hls) player._hls.destroy();
        const hls = new Hls();
        hls.loadSource(src);
        hls.attachMedia(player);
        player._hls = hls;
      } else if (player.canPlayType('application/vnd.apple.mpegurl')) {
        player.src = src;
      }
    } else {
      player.src = src;
    }
  }

  function playCurrent() {
    const clip = window.clipsPlayer.clips[window.clipsPlayer.currentIndex];
    if (!clip) return;
    loadInto(mainPlayer, clip.url);
    mainPlayer.play().catch(() => {});
    const nextClip = window.clipsPlayer.clips[(window.clipsPlayer.currentIndex + 1) % window.clipsPlayer.clips.length];
    if (nextClip) loadInto(preloadPlayer, nextClip.url);
    window.dispatchEvent(new CustomEvent('clips:clipchange', { detail: { clip, index: window.clipsPlayer.currentIndex } }));
  }

  mainPlayer.addEventListener('ended', () => {
    window.dispatchEvent(new CustomEvent('clips:ended'));
    window.clipsPlayer.currentIndex = (window.clipsPlayer.currentIndex + 1) % window.clipsPlayer.clips.length;
    playCurrent();
  });

  // When a clip fails to load (e.g., expired Twitch signed URL), skip it
  mainPlayer.addEventListener('error', () => {
    if (!window.clipsPlayer.ready) return;
    window.clipsPlayer.currentIndex = (window.clipsPlayer.currentIndex + 1) % window.clipsPlayer.clips.length;
    playCurrent();
  });

  window.clipsPlayer.next    = function() { this.currentIndex = (this.currentIndex + 1) % this.clips.length; playCurrent(); };
  window.clipsPlayer.prev    = function() { this.currentIndex = (this.currentIndex - 1 + this.clips.length) % this.clips.length; playCurrent(); };
  window.clipsPlayer.pause   = function() { mainPlayer.pause(); };
  window.clipsPlayer.play    = function() { mainPlayer.play().catch(() => {}); };
  window.clipsPlayer.setVolume = function(v) { mainPlayer.volume = Math.max(0, Math.min(1, v)); };

  (async () => {
    try {
      const all = PLATFORM === 'twitch' ? await fetchTwitchClips() : await fetchKickClips();
      if (!all.length) { console.error(`No clips found for ${PLATFORM}:${CHANNEL}`); return; }
      window.clipsPlayer.clips = shuffle(all);
      window.clipsPlayer.currentIndex = 0;
      window.clipsPlayer.ready = true;
      window.dispatchEvent(new Event('clips:ready'));
      playCurrent();
    } catch (e) { console.error('Clip fetch failed:', e); }
  })();
})();
</script>
```

2 Quick Start

  1. In Meld, create a new spark.
  2. Paste the block above into the spark prompt.
  3. Answer the AI's questions — platform first, then channel name.
  4. Keep answering styling questions until the AI has enough to build.
  5. The AI generates an overlay that reads from window.clipsPlayer and listens for clips:clipchange events.
  6. Want a redesign later? Reuse the same prompt — the data layer stays put.