Shoutout Clip

KICK TWITCH

A Meld spark that plays a random clip from whichever channel you shout out — Kick or Twitch.

1 Copy the Prompt

This block gives Meld's AI everything it needs. Meld handles the chat commands; this prompt just provides the clip-fetching API the widget uses internally.

Build a shoutout clip overlay widget. When triggered, it plays one random clip from the shouted-out channel (Kick or Twitch), with a branded overlay on top (channel avatar, name, CTA), then cleans itself up. ## Data layer — included verbatim at the bottom of this message Include the entire `<script>` block unchanged. It provides two global functions and bundles Hls.js. ### `window.fetchShoutoutClip(platform, channel)` Returns a Promise that resolves to: { platform: "kick" | "twitch", channel: "xqc", channelAvatar: "https://...", // may be empty string channelTitle: "xQc", // display name clip: { id: "string", url: "https://...", // Kick = .m3u8, Twitch = .mp4 title: "clip title", creatorName: "who clipped it", viewCount: 12345, durationMs: 28000 } } Rejects on error (channel not found, no clips, network fail). ### `window.loadShoutoutClip(videoEl, url)` Plays a clip URL in a `<video>` element. Handles `.m3u8` (Kick, via Hls.js) and `.mp4` (Twitch, native) automatically. Pass `null` as the URL to tear down cleanly (clears src and destroys any Hls instance). **Never set `videoEl.src` directly** — Kick clips won't play without Hls.js. Always go through `loadShoutoutClip`. ## Widget contract The widget must listen to live Twitch/Kick chat itself and react to the user's configured commands. Use the Firebase Web SDK (Meld exposes chat messages through Firebase) — load it from the Firebase CDN and subscribe to the chat stream. When a message matches the user's Kick command, fire a Kick shoutout; when it matches the Twitch command, fire a Twitch shoutout. The widget must ALSO expose two trigger paths so the same shoutout flow can be fired without chat (testing, dashboard buttons, etc.): 1. **Global functions:** `window.shoutoutKick(channel)` and `window.shoutoutTwitch(channel)`. 2. **Custom event:** `window.addEventListener('shoutout', e => ...)` where `e.detail = { platform, channel }`. All three trigger paths (chat message, function call, event) must end up in the same place — the fetch/play pipeline. Simplest structure: the chat listener parses messages into `{ platform, channel }`, then calls the same handler the global function / event listener call. One pipeline, three entry points. After setup, the widget should log the two command names to the console (e.g. `Shoutout widget active — !sok for Kick, !sot for Twitch`) so the user can verify everything wired up correctly. ## Behavior - Idle state: fully hidden (transparent background, for OBS browser source). - On trigger: `await fetchShoutoutClip(platform, channel)` → `loadShoutoutClip(videoEl, url)` → play → show overlay with entrance animation. - On `video.ended`: run exit animation, then hide the widget and tear down with `loadShoutoutClip(videoEl, null)`. - If `fetchShoutoutClip` rejects: log to console, stay hidden, don't crash. - If a trigger arrives while a shoutout is playing: queue it. Don't interrupt the current clip. - Long channel names and titles should truncate gracefully (ellipsis or marquee). ## Required inputs — ask the user for these before building - **Kick chat command** — what they type in chat to shout someone out from Kick. Default: `!sok`. Examples: `!sok`, `!shoutoutkick`, `!kickso`, `!plug`. - **Twitch chat command** — same but for Twitch. Default: `!sot`. Examples: `!sot`, `!shoutouttwitch`, `!twitchso`, `!raid`. Validate: must start with `!`, contain only letters/numbers/underscores after the `!`, and the two commands must be different from each other. If the user enters an invalid or duplicate command, re-ask. Both commands take one argument: the channel name being shouted out. Usage in chat will be `<command> <channel>` — for example, if the user picks `!sok` for Kick, they'll type `!sok xqc` in chat to trigger a Kick shoutout for xqc. ## Styling — offer the user these choices before building - **Vibe:** hype/announcement, friendly/cozy, gaming HUD, arcade marquee, cinematic title card, news alert, trading card reveal, esports broadcast, neon sign - **Overlay content:** any combination of channel avatar, channel display name, call-to-action line ("Go follow @X"), clip title, clip creator, or nothing at all - **Entrance animation:** slide in, zoom bounce, type-on text, glitch reveal, iris wipe - **Exit animation:** fade, slide out, glitch, hard cut - **Frame/border:** none, retro TV, neon outline, letterbox, polaroid, arcade cabinet - **Fonts:** any Google Font - **Volume:** default 100%, user-overridable ## Technical constraints - Vanilla JS and CSS, no frameworks. - Transparent background. - The data layer block must be included verbatim. - No hardcoded channel names — the channel arrives via the trigger call. ## Data layer (include verbatim) <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script> <script> (function() { const ENDPOINTS = { kick: 'https://mrboostlive.com/kick/clips/clips.php', twitch: 'https://mrboostlive.com/twitch/clips/clip-data.php' }; // Normalize Kick's response shape into the unified shape function normalizeKick(data, channel) { return { platform: 'kick', channel, channelAvatar: data.channel?.avatar || '', channelTitle: data.channel?.display_name || data.channel?.name || channel, clip: { id: data.id, url: data.clip_url, // .m3u8 HLS — widget must use Hls.js title: data.title || '', creatorName: data.creator?.username || '', viewCount: data.view_count || 0, durationMs: (data.duration || 0) * 1000 } }; } // Normalize Twitch's response shape into the unified shape function normalizeTwitch(data, channel) { const clip = data.clip || {}; return { platform: 'twitch', channel, channelAvatar: data.channel?.avatar || '', channelTitle: data.channel?.display_name || data.channel?.login || channel, clip: { id: clip.id, url: clip.video_url, // signed .mp4 — plays natively title: clip.title || '', creatorName: clip.creator_name || '', viewCount: clip.view_count || 0, durationMs: (clip.duration || 0) * 1000 } }; } window.fetchShoutoutClip = async function(platform, channel) { if (!channel || typeof channel !== 'string') throw new Error('Missing channel name'); const p = String(platform).toLowerCase(); if (!ENDPOINTS[p]) throw new Error(`Unknown platform: ${platform}`); // Kick's clip endpoint returns avatar: null for most channels, so grab // the user profile in parallel and merge it in after normalization. const clipFetch = fetch(`${ENDPOINTS[p]}?channel=${encodeURIComponent(channel)}`); const userFetch = p === 'kick' ? fetch(`https://mrboostlive.com/kick/api/?channel=${encodeURIComponent(channel)}`).catch(() => null) : null; const res = await clipFetch; if (!res.ok) throw new Error(`${p} lookup failed (${res.status})`); const data = await res.json(); const normalized = p === 'twitch' ? normalizeTwitch(data, channel) : normalizeKick(data, channel); if (p === 'kick' && !normalized.channelAvatar && userFetch) { try { const userRes = await userFetch; if (userRes && userRes.ok) { const userData = await userRes.json(); if (userData?.user?.profile_pic) normalized.channelAvatar = userData.user.profile_pic; } } catch (_) { /* non-fatal — just use empty avatar */ } } if (!normalized.clip.url) throw new Error(`No playback URL for ${channel}`); return normalized; }; // Helper for the widget — loads a clip URL into a video element, // transparently handling both .m3u8 (Kick, needs Hls.js) and .mp4 (Twitch, native). // Use this in your "ended"/"error" cleanup: call loadShoutoutClip(videoEl, null) to tear down. window.loadShoutoutClip = function(videoEl, url) { if (videoEl._hls) { try { videoEl._hls.destroy(); } catch (_) {} videoEl._hls = null; } if (!url) { videoEl.removeAttribute('src'); videoEl.load(); return; } if (url.includes('.m3u8') && window.Hls && Hls.isSupported()) { const hls = new Hls(); hls.loadSource(url); hls.attachMedia(videoEl); videoEl._hls = hls; } else { videoEl.src = url; } }; })(); </script>

2 Quick Start

  1. In Meld, create a new spark.
  2. Paste the block above into the spark prompt.
  3. Answer the styling questions — vibe, overlay content, animations, test channel.
  4. Once the widget is built, use Meld's chat-command UI to wire a chat trigger (like !sok <name> or !sot <name>) to call window.shoutoutKick(name) or window.shoutoutTwitch(name).
  5. Name the chat commands whatever fits your chat — the widget doesn't care, it just reacts to the function call.