> ## Documentation Index
> Fetch the complete documentation index at: https://docs.livepeer.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Embody Recent Announcements

> Latest community activity, Discord announcements, and GitHub updates for Embody.

export const embodySocials = [{
  icon: 'globe',
  href: 'https://embody.zone/',
  label: 'Embody'
}, {
  icon: 'github',
  href: 'https://github.com/its-DeFine/Unreal_Vtuber',
  label: 'Embody GitHub'
}, {
  icon: 'youtube',
  href: 'https://www.youtube.com/@Embody-Media',
  label: 'Embody Media YouTube'
}, {
  icon: 'comment-dots',
  href: 'https://forum.livepeer.org/t/embody-unreal-engine-updates/3186',
  label: 'Livepeer Forum'
}, {
  icon: 'discord',
  href: 'https://discord.gg/gVmDMhYm',
  label: 'Embody Discord'
}];

export const embodyDiscordData = [{
  id: "1486302945002913792",
  content: "<@&1409161761105383536> The latest version of the Unreal Vtuber is out, and brings two big quality of life changes<a:party_blob:1026524086714765352> :<br /><br /><strong>1:</strong> a <code>vtuber-auto-updater</code> container is added. New container images with the <code>:latest</code> tag will be automatically pulled  and deployed. This removes the need for the orchestrator to redeploy the stack again whenever we publish a new image. <br /><br /><strong>2:</strong> a <code>vtuber-watchdog</code> container is added. Now every time that the game container exits unexpectedly the stack will automatically recover. This resolves the issue described here <<a href=\"https://github.com/its-DeFine/Unreal_Vtuber/issues/54>\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/its-DeFine/Unreal_Vtuber/issues/54></a><br /><br />The first change mainly addresses the update fatigue, from now on you will need to update only in infrequent major version updates, while we can keep updating our core tech in all orchestrator nodes via a simple image pull. The second change resolves a major bug that required manual orchestrator intervention each time the game unexpectedly existed.<br /><br />Let me know if you have any issue while updating, and please update to the latest version so that you can receive the planned workloads, which will start tomorrow 🙏",
  author: "_alisonwonderland",
  timestamp: "2026-03-25T09:57:16.262Z",
  url: "https://discord.com/channels/1066890817425387581/1485293203681706167/1486302945002913792"
}, {
  id: "1486302905492439201",
  content: "<@&1409161761105383536> The latest version of Unreal Vtuber is up, you will find it here <<a href=\"https://github.com/its-DeFine/Unreal_Vtuber>\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/its-DeFine/Unreal_Vtuber></a> together with README instructions on how to upgrade from a previous version. <br /><br />This updates allow us to finally use your provided compute both for content creation and for powering our upcoming real time app. We also introduced changes that reduce architectural complexity, increase output quality and make future onboarding easier. This update also marks the end of the private alpha and <strong>the start of the public alpha</strong>, we will wait for two days in case that any bug appears and then announce the public release. The time to take the next step has come, thanks to everyone who supported the network by participating in the closed alpha🙏 ❤️ <a:green_flame:1022958061314392104> <br /><br /><strong>Changelog</strong><br /><br /><code><em>Repository</em></code><br /><ul><li>Game container now forces 1080p/60 fps H.264 output; WebRTC captures retain full quality.</li><li>Signaling image ships the proprietary Pixel Streaming UI baked in-no more host bind mount.</li></ul>-Image containers where updated.<br /><ul><li>Script runner shares the game container's network namespace.</li><li>Legacy recorder service removed from the compose stack.</li><li>README split into new deployment vs. upgrade paths with refreshed firewall whitelists (forwarder treated as the primary client) </li><li>GPU specs/reference notes added.</li><li>Whitelisted Client address changed to a newer one.</li></ul><code>Payments</code><br />Payment payout threshold is now at 0.1 ETH instead of 0.001<br /><br /><strong>Notes</strong><br />A new set of incentives and disincentives will be rolled out within the weekend to make sure that updates are happening in a timely manner.",
  author: "_alisonwonderland",
  timestamp: "2026-03-25T09:57:06.842Z",
  url: "https://discord.com/channels/1066890817425387581/1485293203681706167/1486302905492439201"
}, {
  id: "1486302867878051860",
  content: "<@&1409161761105383536> We are going to offer our avatars to encode hackathon participants. The Emnbody team will also enter the hackathon and present you our first commercial app during the hackathon. We expect the orchestrator update to happen tomorrow(BYOC will be added next week due to time constrains). Top ups will be released the same day for everyone. <br /><br />Change Log:<br /><ul><li>a new game container version.</li><li>an updated pixel streaming UI. </li><li>An updated payment processor.</li></ul>",
  author: "_alisonwonderland",
  timestamp: "2026-03-25T09:56:57.874Z",
  url: "https://discord.com/channels/1066890817425387581/1485293203681706167/1486302867878051860"
}, {
  id: "1486302785036095488",
  content: "<@&1409161761105383536> <br /><br /><strong>Orchestrator Logic Updates </strong> <br /><br /><ul><li>New update is coming later today along with the next wallet top up, this will require orchestrators to rebuild and restart the containers. We will introduce a Grace period for Orchestrators implementing the updates, orchestrators who do not update within the Grace period will still receive payouts, although those payouts will be reduced. Once you update, the payout amount will immediately come back to it's original value.</li><li><@379220097132396554>  created a PR which allows the unreal engine game to use BYOC streaming<<a href=\"https://github.com/its-DeFine/Unreal_Vtuber/pull/42>.\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/its-DeFine/Unreal_Vtuber/pull/42>.</a> We are working on getting back to BYOC and replacing the payment processor(keeping it only as fallback). This is also dependent on the Livepeer-Go release schedule as there are PRs for fixing the legacy gas costs issue(which rendered our latest BYOC pipeline practically practically not functional) and the introduction of the ability for orchestrators to ignore the ticket's sender reserve.</li></ul><strong>Payment Processor Updates</strong><br /><br />Our payment processor received two PRs which allowed the following:<br /><br /><strong>1.</strong> The ability to create a <strong>Denylist</strong> in the config to prevent abuse. <em>This was done to prevent balance drain from Orchestrators with more than one nodes in the top 100 and will not be enforced in the official release.</em><br /><br /><strong>2.</strong> Now when an address that is not in the admin allowlist calls the <code>orchestrators</code> api endpoint  with <code>curl <a href=\"http://3.141.111.200:8081/api/orchestrators/\" target=\"_blank\" rel=\"noopener noreferrer\">http://3.141.111.200:8081/api/orchestrators/</a></code> it will get redacted fields of sensitive information.<br /><strong>Note:</strong> To maintain transparency you are still able to trigger the call endpoint and view the non redacted data of all participant orchestrators from any ip.",
  author: "_alisonwonderland",
  timestamp: "2026-03-25T09:56:38.123Z",
  url: "https://discord.com/channels/1066890817425387581/1485293203681706167/1486302785036095488"
}, {
  id: "1485316137775530244",
  content: "Embody #announcements",
  author: "_alisonwonderland",
  timestamp: "2026-03-22T16:36:03.085Z",
  url: "https://discord.com/channels/1066890817425387581/1485293203681706167/1485316137775530244"
}];

export const youtubeData = [{
  title: 'Luna AI Avatar - Interactive Live Stream',
  href: 'https://www.youtube.com/watch?v=b9QAHQwHUyg',
  author: 'By Embody Media',
  content: 'Interactive AI avatar stream. Say hi in chat and Luna will respond!...',
  publishedDate: 'Jan 16, 2026',
  duration: 'PT52M33S',
  thumbnailUrl: 'https://i.ytimg.com/vi/b9QAHQwHUyg/hqdefault.jpg'
}, {
  title: 'Unreal Vtuber v0.2 - Embodied AI with Avatar Roles',
  href: 'https://www.youtube.com/watch?v=_MAM5ZPsTdM',
  author: 'By Embody Media',
  content: 'https://github.com/its-DeFine/Unreal_Vtuber  #EmbodiedAI #MultiAgentAI #UnrealEngineAI #UnrealEngine5 #AIAvatarSystem #MetaHumanAI #Convai #RealTimeAIAvatars #NPC_AI_Unreal #ReinforcementLearningUE #AICrowdSimulation #UnrealMAP #UnrealZoo #VectorFlowFieldNav #UEFN #FortniteAI #StateOfUnreal #AIDarthVader #AICharacters #GameDevAI...',
  publishedDate: 'Jul 15, 2025',
  duration: 'PT5M17S',
  thumbnailUrl: 'https://i.ytimg.com/vi/_MAM5ZPsTdM/hqdefault.jpg'
}, {
  title: 'Define & Dane | Live in Lisbon Summit 2025',
  href: 'https://www.youtube.com/watch?v=s1dhZTofuCM',
  author: 'By Livepeer',
  content: 'Define & Dane present Embody at Live in Lisbon Summit 2025.',
  publishedDate: 'Nov 15, 2025',
  duration: '',
  thumbnailUrl: 'https://img.youtube.com/vi/s1dhZTofuCM/hqdefault.jpg'
}];

export const BorderedBox = ({children, variant = "default", padding = "var(--lp-spacing-4)", borderRadius = "var(--lp-spacing-px-8)", margin = "", accentBar = "", style = {}, className = "", ...rest}) => {
  const variants = {
    default: {
      border: "1px solid var(--lp-color-border-default)",
      backgroundColor: "var(--lp-color-bg-card)"
    },
    accent: {
      border: "1px solid var(--lp-color-accent)",
      backgroundColor: "var(--lp-color-bg-card)"
    },
    muted: {
      border: "1px solid var(--lp-color-border-default)",
      backgroundColor: "transparent"
    }
  };
  const accentBarColors = {
    accent: "var(--lp-color-accent)",
    positive: "var(--green-9)"
  };
  return <div data-docs-bordered-box="" data-accent-bar={accentBarColors[accentBar] ? "" : undefined} className={className} style={{
    ...variants[variant],
    padding: padding,
    borderRadius: borderRadius,
    ...margin ? {
      margin
    } : {},
    ...accentBarColors[accentBar] ? {
      position: "relative",
      '--accent-bar-color': accentBarColors[accentBar]
    } : {},
    ...style
  }} {...rest}>
      {children}
    </div>;
};

export const CenteredContainer = ({children, maxWidth = "800px", padding = "0", preset = "default", width = "", minWidth = "", marginRight = "", marginBottom = "", textAlign = "", style = {}, className = "", ...rest}) => {
  const presets = {
    default: {},
    fitContent: {
      width: "fit-content",
      minWidth: "fit-content"
    },
    readable70: {
      width: "70%",
      minWidth: "fit-content"
    },
    readable80: {
      width: "80%",
      minWidth: "fit-content"
    },
    readable90: {
      width: "90%"
    },
    wide900: {
      maxWidth: "900px"
    }
  };
  const presetStyle = presets[preset] || presets.default;
  return <div className={className} style={{
    maxWidth: presetStyle.maxWidth || maxWidth,
    margin: "0 auto",
    padding: padding,
    ...presetStyle.width ? {
      width: presetStyle.width
    } : {},
    ...presetStyle.minWidth ? {
      minWidth: presetStyle.minWidth
    } : {},
    ...width ? {
      width
    } : {},
    ...minWidth ? {
      minWidth
    } : {},
    ...marginRight ? {
      marginRight
    } : {},
    ...marginBottom ? {
      marginBottom
    } : {},
    ...textAlign ? {
      textAlign
    } : {},
    ...style
  }} {...rest}>
      {children}
    </div>;
};

export const LazyLoad = ({children, height = "200px", offset = "200px", fadeDuration = 400, className = "", style = {}, ...rest}) => {
  const ref = useRef(null);
  const [visible, setVisible] = useState(false);
  const [ready, setReady] = useState(false);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        setVisible(true);
        observer.disconnect();
      }
    }, {
      rootMargin: offset
    });
    observer.observe(el);
    return () => observer.disconnect();
  }, []);
  useEffect(() => {
    if (!visible) return;
    const frameId = requestAnimationFrame(() => {
      setReady(true);
    });
    return () => cancelAnimationFrame(frameId);
  }, [visible]);
  const placeholder = <div ref={ref} className={className} style={{
    minHeight: height,
    ...style
  }} {...rest} />;
  if (!visible) return placeholder;
  return <div ref={ref} className={className} style={{
    opacity: ready ? 1 : 0,
    transition: `opacity ${fadeDuration}ms ease-in`,
    ...style
  }} {...rest}>
      {children}
    </div>;
};

export const ScrollBox = ({children, maxHeight = 300, showHint = true, ariaLabel = "Scrollable content", style = {}, className = "", ...rest}) => {
  const contentRef = useRef(null);
  const [isOverflowing, setIsOverflowing] = useState(false);
  useEffect(() => {
    const checkOverflow = () => {
      if (contentRef.current) {
        const maxHeightPx = typeof maxHeight === "number" ? maxHeight : parseInt(maxHeight, 10) || 300;
        setIsOverflowing(contentRef.current.scrollHeight > maxHeightPx);
      }
    };
    checkOverflow();
    window.addEventListener("resize", checkOverflow);
    return () => window.removeEventListener("resize", checkOverflow);
  }, [maxHeight, children]);
  return <div className={className} style={{
    position: "relative",
    ...style
  }} {...rest}>
      <div ref={contentRef} role="region" tabIndex={0} aria-label={ariaLabel} style={{
    maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight,
    overflowY: "auto",
    paddingRight: 4
  }} onScroll={e => {
    const el = e.target;
    const atBottom = el.scrollHeight - el.scrollTop <= el.clientHeight + 10;
    const hint = el.parentNode.querySelector("[data-scroll-hint]");
    if (hint) hint.style.opacity = atBottom ? "0" : "1";
  }}>
        {children}
      </div>
      {showHint && isOverflowing && <div data-scroll-hint style={{
    fontSize: 11,
    color: "var(--lp-color-text-muted)",
    textAlign: "center",
    marginTop: 8,
    transition: "opacity 0.2s"
  }}>
          Scroll for more ↓
        </div>}
    </div>;
};

export const CustomDivider = ({color = "var(--lp-color-border-default)", middleText = "", spacing = "default", style = {}, className = "", ...rest}) => {
  const spacingPresets = {
    default: {
      margin: "24px 0"
    },
    overlap: {
      margin: "-1rem 0 -1rem 0"
    },
    tight: {
      margin: "0 0 -1rem 0"
    },
    section: {
      margin: "0 0 -2rem 0"
    },
    sectionOverlap: {
      margin: "-1rem 0 -2rem 0"
    },
    deepOverlap: {
      margin: "-1rem 0 -1.5rem 0"
    }
  };
  const spacingStyle = spacingPresets[spacing] || spacingPresets.default;
  return <div role="separator" aria-orientation="horizontal" className={className} style={{
    display: "flex",
    alignItems: "center",
    ...spacingStyle,
    fontSize: style?.fontSize || "16px",
    height: "fit-content",
    ...style
  }} {...rest}>
      <span style={{
    marginRight: "var(--lp-spacing-px-8)",
    opacity: 0.2
  }}>
        <Icon icon="/snippets/assets/logos/Livepeer-Logo-Symbol-Theme.svg" />
      </span>
      <div style={{
    flex: 1,
    height: "1px",
    background: "var(--lp-color-border-default)",
    opacity: 0.4
  }}></div>
      {middleText && <>
          <Icon icon="circle" size={2} />
          <span style={{
    margin: "0 8px",
    fontWeight: "bold",
    color: color,
    opacity: 0.7
  }}>
            {middleText}
          </span>
          <Icon icon="circle" size={2} />
        </>}
      <div style={{
    flex: 1,
    height: "1px",
    background: "var(--lp-color-border-default)",
    opacity: 0.4
  }}></div>
      <span style={{
    marginLeft: "var(--lp-spacing-px-8)",
    opacity: 0.2
  }}>
        <span style={{
    display: "inline-block",
    transform: "scaleX(-1)"
  }}>
          <Icon icon="/snippets/assets/logos/Livepeer-Logo-Symbol-Theme.svg" />
        </span>
      </span>
    </div>;
};

export const MarkdownEmbed = ({url, className = '', style = {}, ...rest}) => {
  const [html, setHtml] = useState('');
  useEffect(() => {
    fetch(url).then(res => res.text()).then(md => {
      const converted = md.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>').replace(/`([^`]+)`/g, '<code>$1</code>').replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img alt="$1" src="$2" style="max-width:100%" />').replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>').replace(/^######\s+(.+?)(?:\s+#+)?$/gm, '<h6>$1</h6>').replace(/^#####\s+(.+?)(?:\s+#+)?$/gm, '<h5>$1</h5>').replace(/^####\s+(.+?)(?:\s+#+)?$/gm, '<h4>$1</h4>').replace(/^###\s+(.+?)(?:\s+#+)?$/gm, '<h3>$1</h3>').replace(/^##\s+(.+?)(?:\s+#+)?$/gm, '<h2>$1</h2>').replace(/^#\s+(.+?)(?:\s+#+)?$/gm, '<h1>$1</h1>').replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>').replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>').replace(/\*(.+?)\*/g, '<em>$1</em>').replace(/^---$/gm, '<hr />').replace(/^[-*]\s+(.+)$/gm, '<li>$1</li>').replace(/\n\n/g, '</p><p>').replace(/\n/g, '<br />');
      setHtml('<p>' + converted + '</p>');
    });
  }, [url]);
  if (!html) return <div className={className} style={style} {...rest}>
        <p style={{
    color: 'var(--text-secondary)'
  }}>Loading...</p>
      </div>;
  return <div className={className} {...rest} style={{
    maxWidth: '100%',
    overflowWrap: 'anywhere',
    wordBreak: 'break-word',
    ...style
  }} dangerouslySetInnerHTML={{
    __html: html
  }} />;
};

export const DiscordAnnouncements = ({serverName = 'Livepeer', items = [], limit, ScrollBox, scrollMaxHeight = 300, className = '', style = {}, ...rest}) => {
  const sanitiseHTML = html => {
    if (!html || typeof html !== 'string') return '';
    return html.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<iframe[\s\S]*?<\/iframe>/gi, '').replace(/<object[\s\S]*?<\/object>/gi, '').replace(/<embed[\s\S]*?>/gi, '').replace(/<form[\s\S]*?<\/form>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '').replace(/<link[\s\S]*?>/gi, '').replace(/\bon\w+\s*=\s*(['"])[^'"]*\1/gi, '').replace(/\bon\w+\s*=\s*[^\s>]*/gi, '').replace(/javascript\s*:/gi, '');
  };
  const displayItems = limit ? items.slice(0, limit) : items;
  if (!displayItems || displayItems.length === 0) {
    return <Note>
        <p style={{
      color: 'var(--text-secondary)',
      textAlign: 'center'
    }}>
          No announcements at this time.
        </p>
      </Note>;
  }
  const parseContent = content => {
    const withLinks = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1 ↗</a>');
    return withLinks;
  };
  return <div className={className} style={{
    display: 'flex',
    flexDirection: 'column',
    gap: "var(--lp-spacing-4)",
    border: '1px solid var(--lp-color-accent)',
    borderRadius: "var(--lp-spacing-2)",
    padding: "var(--lp-spacing-4)",
    ...style
  }} {...rest}>
      {displayItems.map((announcement, index) => <div key={announcement.id} href={announcement.url} target="_blank" rel="noopener noreferrer">
          <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: "var(--lp-spacing-2)",
    fontSize: '0.875rem',
    marginBottom: "var(--lp-spacing-3)",
    width: '100%'
  }}>
            <Icon icon="discord" color="var(--lp-color-brand-discord)" size={16} />
            <span style={{
    fontWeight: 600,
    color: 'var(--lp-color-accent)',
    fontSize: 'medium'
  }}>
              {serverName}
              {}
            </span>
            <span style={{
    color: 'var(--lp-color-text-secondary)'
  }}>•</span>
            <time dateTime={announcement.timestamp} style={{
    color: 'var(--lp-color-text-secondary)'
  }}>
              {new Date(announcement.timestamp).toLocaleDateString('en-US', {
    month: 'short',
    day: 'numeric',
    year: 'numeric'
  })}
            </time>
            <span style={{
    fontSize: '0.875rem',
    color: 'var(--lp-color-text-secondary)',
    marginLeft: 'auto'
  }}>
              View in Discord{' '}
              <Icon icon="arrow-up-right" size={12} color="var(--lp-color-accent)" />
            </span>
          </div>
          <ScrollBox maxHeight={scrollMaxHeight} ariaLabel={`Announcement from ${serverName}`}>
            <div style={{
    color: 'var(--lp-color-text-secondary)',
    fontSize: 'small'
  }} dangerouslySetInnerHTML={{
    __html: sanitiseHTML(parseContent(announcement.content))
  }} />
          </ScrollBox>
          {index < displayItems.length - 1 && <div style={{
    marginTop: "var(--lp-spacing-3)"
  }}>
              <hr style={{
    border: 'none',
    borderBottom: '1px solid var(--lp-color-border-default)',
    margin: '0'
  }} />
            </div>}
        </div>)}
    </div>;
};

export const YouTubeVideoData = ({items = [], limit, cols = 2, className = "", style = {}, ...rest}) => {
  const displayItems = limit ? items.slice(0, limit) : items;
  if (!displayItems || displayItems.length === 0) {
    return <Note>
        <p style={{
      color: "var(--text-secondary)",
      textAlign: "center"
    }}>
          No videos at this time.
        </p>
      </Note>;
  }
  const getEmbedUrl = href => {
    if (!href) return "";
    const videoId = href.split("v=")[1]?.split("&")[0];
    return videoId ? `https://www.youtube.com/embed/${videoId}` : href;
  };
  return <Columns cols={cols} className={className} style={style} {...rest}>
      {displayItems.map((item, idx) => {
    if (!item || !item.href) return null;
    const needsSpacer = idx > 0 && idx % cols === 0;
    return <>
            {needsSpacer && <div key={`spacer-${idx}`} style={{
      height: "var(--lp-spacing-6)",
      width: "100%"
    }} />}
            {needsSpacer && <div key={`spacer2-${idx}`} style={{
      height: "var(--lp-spacing-6)",
      width: "100%"
    }} />}
            <YouTubeVideo key={item.href || idx} embedUrl={getEmbedUrl(item.href)} title={item.title || ""} caption={`${item.author || ""} • ${item.publishedDate || ""}`} />
          </>;
  })}
    </Columns>;
};

export const YouTubeVideo = ({embedUrl, url, title = "", author = "", hint = "", caption, className = "", style = {}, ...rest}) => {
  const toEmbedUrl = value => {
    if (!value || typeof value !== "string") return "";
    const source = value.trim();
    if (!source) return "";
    try {
      const parsed = new URL(source);
      const host = parsed.hostname.replace(/^www\./, "");
      if (host === "youtube.com" || host === "youtube-nocookie.com") {
        if (parsed.pathname.startsWith("/embed/")) return source;
        const videoId = parsed.pathname.startsWith("/shorts/") ? parsed.pathname.split("/").filter(Boolean)[1] : parsed.searchParams.get("v");
        if (!videoId) return "";
        const params = new URLSearchParams(parsed.search);
        params.delete("v");
        const query = params.toString();
        return `https://www.youtube.com/embed/${videoId}${query ? `?${query}` : ""}`;
      }
      if (host === "youtu.be") {
        const videoId = parsed.pathname.split("/").filter(Boolean)[0];
        if (!videoId) return "";
        const query = parsed.searchParams.toString();
        return `https://www.youtube.com/embed/${videoId}${query ? `?${query}` : ""}`;
      }
    } catch (_err) {
      return "";
    }
    return "";
  };
  const resolvedEmbedUrl = toEmbedUrl(embedUrl || url);
  if (!resolvedEmbedUrl) {
    return null;
  }
  const isValidYouTubeUrl = resolvedEmbedUrl.includes("youtube.com/embed/");
  if (!isValidYouTubeUrl) {
    console.warn("Invalid YouTube embed URL:", embedUrl || url);
    return null;
  }
  const buildCaption = () => {
    if (caption) return caption;
    if (!author && !title) return null;
    return <span style={{
      display: "flex",
      flexDirection: "column",
      alignItems: "center",
      textAlign: "center",
      lineHeight: 1.2
    }}>
        <span>
          {author && <>
              <Icon icon="microphone" size={16} /> <strong>{author}</strong>
            </>}
          {author && title ? `${" "} • ${title}` : title}
        </span>
      </span>;
  };
  const captionContent = buildCaption();
  return <Frame className={className} style={style} {...hint ? {
    hint
  } : {}} {...captionContent ? {
    caption: captionContent
  } : {}} {...rest}>
      <iframe className="w-full aspect-video rounded-xl" src={resolvedEmbedUrl} title={title || author || "YouTube Video"} allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen />
    </Frame>;
};

export const LinkArrow = ({href, label, description, newline = true, borderColor, className = '', style = {}, ...rest}) => {
  const linkArrowStyle = {
    display: 'inline-flex',
    alignItems: 'center',
    justifyContent: 'center',
    gap: "var(--lp-spacing-1)",
    width: 'fit-content',
    ...borderColor && ({
      borderColor
    })
  };
  return <span className={className} style={style} {...rest}>
      {newline && <br />}
      <span style={linkArrowStyle}>
        <a href={href} target="_blank" rel="noopener noreferrer">
          {label}
        </a>
        <Icon icon="arrow-up-right" size={14} color="var(--lp-color-accent)" />
      </span>
      {description && description}
      {description && <div style={{
    height: "var(--lp-spacing-3)"
  }} />}
    </span>;
};

export const SocialLinks = ({links, size = 20, gap = "var(--lp-spacing-3)", justify = "center", iconColor, color, className = "", style = {}, ...rest}) => {
  const resolvedIconColor = iconColor || color;
  const linkStyle = {
    border: "none",
    borderBottom: "none",
    textDecoration: "none",
    display: "inline-flex"
  };
  const colors = {
    discord: resolvedIconColor || "var(--lp-color-brand-discord)",
    twitter: resolvedIconColor || "var(--lp-color-text-primary)",
    github: resolvedIconColor || "var(--lp-color-brand-github)",
    forum: resolvedIconColor || "var(--lp-color-brand-forum)",
    website: resolvedIconColor || "var(--lp-color-accent)",
    blog: resolvedIconColor || "var(--lp-color-accent)",
    globe: resolvedIconColor || "var(--lp-color-brand-globe)",
    twitch: resolvedIconColor || "var(--lp-color-brand-twitch)",
    youtube: resolvedIconColor || "var(--lp-color-brand-youtube)",
    instagram: resolvedIconColor || "var(--lp-color-brand-instagram)",
    linkedin: resolvedIconColor || "var(--lp-color-brand-linkedin)"
  };
  const iconColorMap = {
    discord: "discord",
    "x-twitter": "twitter",
    github: "github",
    "comment-pen": "forum",
    "pen-line": "blog",
    "pencil-line": "blog",
    globe: "globe",
    "book-open": "website",
    twitch: "twitch",
    youtube: "youtube",
    instagram: "instagram",
    linkedin: "linkedin"
  };
  const defaultLinks = [{
    icon: "discord",
    href: "https://discord.com/invite/livepeer",
    label: "Livepeer Discord"
  }, {
    icon: "globe",
    href: "https://livepeer.org",
    label: "Livepeer Website"
  }, {
    icon: "github",
    href: "https://github.com/livepeer",
    label: "Livepeer GitHub"
  }, {
    icon: "comment-pen",
    href: "https://forum.livepeer.org",
    label: "Livepeer Forum"
  }, {
    icon: "pen-line",
    href: "https://livepeer.org/blog",
    label: "Livepeer Blog"
  }, {
    icon: "x-twitter",
    href: "https://x.com/livepeer",
    label: "Livepeer X"
  }];
  const items = links || defaultLinks;
  return <div className={className} style={style} {...rest}>
      <style>{`
        .social-links a {
          border: none;
          border-bottom: none;
        }
      `}</style>
      <span className="social-links" style={{
    display: "flex",
    justifyContent: justify,
    gap: gap,
    marginTop: "var(--lp-spacing-2)"
  }}>
        {items.map((item, i) => <a key={i} href={item.href} target="_blank" rel="noopener noreferrer" aria-label={item.label} style={linkStyle}>
            <Tooltip headline={item.label}>
              <Icon icon={item.icon} size={size} color={colors[iconColorMap[item.icon] || "website"] || "var(--lp-color-accent)"} aria-hidden="true" />
            </Tooltip>
          </a>)}
      </span>
    </div>;
};

{/* DATA */}

<CenteredContainer maxWidth="fit-content">
  <BorderedBox margin="0" padding="0.5rem 1rem 1rem 1rem" variant="muted">
    <SocialLinks iconColor="var(--hero-text)" links={embodySocials} />
  </BorderedBox>
</CenteredContainer>

<br />

<CenteredContainer width="80%" minWidth="fit-content">
  <Tip>
    This page is an automated workflow.
  </Tip>
</CenteredContainer>

## <Icon icon="camera-movie" size={24} /> Videos

Watch the latest on the <LinkArrow label="Embody Media YouTube Channel" href="https://www.youtube.com/@Embody-Media" newline={false} />

<LazyLoad height="400px">
  <YouTubeVideoData items={youtubeData} limit={4} />
</LazyLoad>

<CustomDivider style={{margin: "0.5rem 0 -2.5rem 0"}} />

## <Icon icon="discord" size={24} /> Discord Announcements

Latest from the <LinkArrow label="Embody Discord" href="https://discord.gg/gVmDMhYm" newline={false} />

<LazyLoad height="300px">
  <DiscordAnnouncements serverName="Embody" items={embodyDiscordData} limit={4} ScrollBox={ScrollBox} scrollMaxHeight={200} />
</LazyLoad>

<CustomDivider style={{margin: "0 0 -2.5rem 0"}} />

## Featured X Post <Icon icon="x-twitter" size={24} />

<LazyLoad height="650px">
  <iframe src="https://platform.twitter.com/embed/Tweet.html?id=2011549141886022107&theme=dark" className="lp-tweet-frame-large" title="Embody on X" />
</LazyLoad>

<CustomDivider style={{margin: "0 0 -2.5rem 0"}} />

## <Icon icon="github" size={24} /> GitHub

<LazyLoad height="600px">
  <BorderedBox>
    <ScrollBox maxHeight={600}>
      <BorderedBox variant="accent">
        Embody Github

        <LinkArrow label="View Embody on GitHub" href="https://github.com/its-DeFine/Unreal_Vtuber" newline={true} />
      </BorderedBox>

      <MarkdownEmbed url="https://raw.githubusercontent.com/its-DeFine/Unreal_Vtuber/main/README.md" />
    </ScrollBox>
  </BorderedBox>
</LazyLoad>

<CustomDivider style={{margin: "0 0 -2.5rem 0"}} />

## Related Pages

<CardGroup cols={2}>
  <Card title="Embody Overview" icon="book-open" href="/solutions/embody/overview" />

  <Card title="Embody Changelog" icon="clock" href="/solutions/embody/changelog" />
</CardGroup>
