Skip to main content
. Not present in production builds. Safe no-op in prod. */ body { padding: 0 !important; } /* Colors Used #3CB540 - Jade Green #2b9a66 - Light Green #18794E - Dark Green Complementary Greens See https://coolors.co/004225-1a794e-08a045-3cb540-62ba4f #004225 - Deep Forrest #1A794E - Turf Green #08A045 - Medium Jungle #3CB540 - Jade Green #6BBF59 - Moss Green See https://coolors.co/0c0c0c-073b3a-1a794e-08a045-6bbf59 #0C0C0C - Onyx Black #073B3A - Dark Teal #1A794E - Turf Green #08A045 - Medium Jungle #6BBF59 - Moss Green See https://coolors.co/fffffa-073b3a-1a794e-08a045-6bbf59 #FFFFFA - Porcelain #073B3A - Dark Teal #1A794E - Turf Green #08A045 - Medium Jungle #6BBF59 - Moss Green Pink Offset Colour See https://coolors.co/073b3a-1a794e-f61067-08a045-6bbf59 #F61067 - Razzmatazz Pink #073B3A - Dark Teal #1A794E - Turf Green #08A045 - Medium Jungle #6BBF59 - Moss Green */ /* ============================================ GLOBAL THEME VARIABLES Component governance source of truth ============================================ */ :root { --lp-color-accent: #3cb540; --lp-color-accent-strong: #18794e; --lp-color-accent-soft: #6bbf59; --lp-color-accent-bright: #5dd662; --lp-color-accent-brightest: #a0f0a5; --lp-color-arbitrum: #3ea6f8; --lp-color-text-primary: #181c18; --lp-color-text-secondary: #717571; --lp-color-text-muted: #9ca3af; --lp-color-bg-page: #ffffff; --lp-color-bg-card: #f9fafb; --lp-color-bg-elevated: #f3f6f4; --lp-color-bg-subtle: rgba(24, 28, 24, 0.04); --lp-color-bg-overlay: rgba(12, 12, 12, 0.5); --lp-color-border-default: #e5e7eb; --lp-color-border-strong: rgba(24, 28, 24, 0.18); --lp-color-border-inverse: rgba(255, 255, 255, 0.5); --lp-color-on-accent: #ffffff; --lp-color-link: #18794e; --lp-color-link-hover: #004225; --lp-color-brand-discord: #5865f2; --lp-color-brand-forum: #00aeef; --lp-color-brand-github: #181c18; --lp-color-brand-x: #181c18; --lp-color-brand-globe: #00c0ff; --lp-color-brand-twitch: #9048ff; --lp-color-brand-youtube: #ff0034; --lp-color-brand-instagram: #dc2275; --lp-color-brand-linkedin: #0189df; --lp-color-brand-preview: #b636dd; --lp-color-brand-coming-soon: #ef1a73; --lp-color-brand-linux: #ff9a0e; --lp-color-brand-windows: #14bbf7; --lp-color-brand-macos: #60ba47; --lp-color-status-good: #22c55e; --lp-color-status-warn: #fbbf24; --lp-color-status-bad: #ef4444; --lp-spacing-1: 0.25rem; --lp-spacing-2: 0.5rem; --lp-spacing-3: 0.75rem; --lp-spacing-4: 1rem; --lp-spacing-6: 1.5rem; --lp-spacing-8: 2rem; --lp-spacing-px-3: 3px; --lp-spacing-px-4: 4px; --lp-spacing-px-6: 6px; --lp-spacing-px-8: 8px; --lp-spacing-px-12: 12px; --lp-font-sans: 'Inter', 'Segoe UI', sans-serif; --lp-font-mono: 'SFMono-Regular', 'SF Mono', 'Menlo', monospace; --lp-radius-sm: 0.25rem; --lp-radius-md: 0.5rem; --lp-radius-lg: 0.75rem; --lp-shadow-card: 0 8px 24px rgba(24, 28, 24, 0.08); --lp-z-base: 1; --lp-z-overlay: 10; --lp-z-modal: 50; /* Legacy aliases maintained during migration */ --accent: var(--lp-color-accent); --accent-dark: var(--lp-color-accent-strong); --hero-text: var(--lp-color-text-primary); --text: var(--lp-color-text-secondary); --text-secondary: var(--lp-color-text-secondary); --muted-text: var(--lp-color-text-muted); --background: var(--lp-color-bg-page); --card-background: var(--lp-color-bg-card); --background-highlight: var(--lp-color-bg-subtle); --border: var(--lp-color-border-default); --button-text: var(--lp-color-on-accent); --page-header-description-color: var(--lp-color-text-secondary); --arbitrum: var(--lp-color-arbitrum); } .dark { --lp-color-accent: #2b9a66; --lp-color-accent-strong: #18794e; --lp-color-accent-soft: #3cb540; --lp-color-accent-bright: #5dd662; --lp-color-accent-brightest: #7fe584; --lp-color-text-primary: #e0e4e0; --lp-color-text-secondary: #a0a4a0; --lp-color-text-muted: #6b7280; --lp-color-bg-page: #0d0d0d; --lp-color-bg-card: #1a1a1a; --lp-color-bg-elevated: #141a16; --lp-color-bg-subtle: rgba(255, 255, 255, 0.1); --lp-color-bg-overlay: rgba(0, 0, 0, 0.5); --lp-color-border-default: #333333; --lp-color-border-strong: rgba(255, 255, 255, 0.3); --lp-color-border-inverse: rgba(255, 255, 255, 0.5); --lp-color-on-accent: #ffffff; --lp-color-link: #5dd662; --lp-color-link-hover: #a0f0a5; --lp-color-brand-github: #f0f0f0; /* Legacy aliases maintained during migration */ --accent: var(--lp-color-accent); --accent-dark: var(--lp-color-accent-strong); --hero-text: var(--lp-color-text-primary); --text: var(--lp-color-text-secondary); --text-secondary: var(--lp-color-text-secondary); --muted-text: var(--lp-color-text-muted); --background: var(--lp-color-bg-page); --card-background: var(--lp-color-bg-card); --background-highlight: var(--lp-color-bg-subtle); --border: var(--lp-color-border-default); --button-text: var(--lp-color-on-accent); --page-header-description-color: var(--lp-color-text-secondary); --arbitrum: var(--lp-color-arbitrum); } /* ============================================ */ /* Code block themes hiki codeblock themes: Popular Dark Themes: github-dark (what you have now) github-dark-dimmed github-dark-high-contrast dracula dracula-soft monokai nord one-dark-pro poimandres rose-pine everforest-dark vitesse-dark Popular Light Themes: github-light (what you have now) github-light-high-contrast solarized-light rose-pine-dawn everforest-light vitesse-light */ /* img[alt="dark logo"], img[alt="light logo"] { max-width: 180px; } */ /* V2 TEST */ /* a.nav-tabs-item[href="/pages/resources/resources_hub.mdx"], a.nav-tabs-item[href="/pages/08_help/README"] { color: rgba(255, 90, 90, 0.342) !important; } */ /* Make the nav-tabs container full width */ .nav-tabs { width: 100%; justify-content: flex-start; } /* Fix Mintlify content width and centering. Regular pages: balance padding + widen inner cap. Portal/frame pages: balance padding (smaller) + widen inner cap for full-width hero. */ @media (min-width: 1024px) { /* Regular pages */ #content-container:not(:has(.frame-mode-hero-full)):not( :has(.frame-mode-container) ) { padding-left: 3rem !important; padding-right: 3rem !important; } #content-container:not(:has(.frame-mode-hero-full)):not( :has(.frame-mode-container) ) > .max-w-5xl { max-width: 72rem !important; } /* Portal/frame pages — tighter balanced padding, wider inner cap */ #content-container:has(.frame-mode-hero-full), #content-container:has(.frame-mode-container) { padding-left: 2rem !important; padding-right: 2rem !important; } #content-container:has(.frame-mode-hero-full) > .max-w-5xl, #content-container:has(.frame-mode-container) > .max-w-5xl { max-width: 80rem !important; } } #navbar > div.z-10.mx-auto.relative > div.hidden.lg\:flex.px-12.h-12 > div { column-gap: 2rem !important; } a.nav-tabs-item[href*='/internal/'] { margin-left: 1rem; margin-right: -1rem; padding-right: 0; border-bottom-color: transparent !important; } /* .gap-x-6 { column-gap: 2rem !important; } */ /* .nav-tabs h-full flex text-sm gap-x-6 { column-gap: 2rem !important; } */ /* Push Resource HUB to the right and style as outlined button */ a.nav-tabs-item[href$='/resources/redirect'], a.nav-tabs-item[href$='/resources/portal'], a.nav-tabs-item[href$='/07_resources/redirect'], a.nav-tabs-item[href$='/07_resources/portal'] { margin-left: auto; background-color: transparent; border: 1px solid var(--accent) !important; padding: 4px 8px; border-radius: 4px; font-size: 0.7rem; height: auto !important; align-self: center; margin-right: -2rem; } /* Color the text */ /* a.nav-tabs-item[href="/v2/resources/resources_hub"] { color: #2b9a66 !important; } */ /* Shrink & color the icon */ a.nav-tabs-item[href$='/resources/redirect'] svg, a.nav-tabs-item[href$='/resources/portal'] svg, a.nav-tabs-item[href$='/07_resources/redirect'] svg, a.nav-tabs-item[href$='/07_resources/portal'] svg, a.nav-tabs-item[href$='/07_resources/resources_hub'] svg { height: 0.75rem; width: 0.75rem; /* background-color: #2b9a66 !important; */ } /* Hide the underline on the button */ a.nav-tabs-item[href$='/resources/redirect'] > div:last-child, a.nav-tabs-item[href$='/resources/portal'] > div:last-child, a.nav-tabs-item[href$='/07_resources/redirect'] > div:last-child, a.nav-tabs-item[href$='/07_resources/portal'] > div:last-child, a.nav-tabs-item[href$='/07_resources/resources_hub'] > div:last-child { display: none; } /* Stack footer links vertically */ #footer .flex-col .flex.gap-4 { flex-direction: column !important; gap: 0rem !important; } /* Reduce footer padding */ #footer > div { padding-top: 2rem !important; padding-bottom: 1rem !important; } /* Accessibility: prevent hidden assistant sheet from receiving focus */ #chat-assistant-sheet[aria-hidden='true'] { display: none !important; } /* Accessibility: ensure CTA buttons meet minimum target size */ button.text-left.text-gray-600.text-sm.font-medium { min-height: 24px; padding-top: 4px; padding-bottom: 4px; } /* #footer > div > div:first-child { display: flex; flex-direction: row !important; color: red !important; } #footer > div > div:first-child > div { display: flex; flex-direction: row !important; color: green !important; } */ /* Fix bad styling of cards with arrows */ [data-component-part='card-content-container'] { padding-right: 2.5rem; /* Creates space for the arrow */ } /* Reposition View component dropdown */ /* To find the correct selector: 1. Open your page with View components in the browser 2. Right-click on the dropdown in the top-right corner 3. Select "Inspect Element" 4. Find the class name or data attribute 5. Replace the selector below with the actual one */ /* Common possible selectors - uncomment and adjust the one that works */ /* Option 1: If it has a data attribute */ /* [data-view-dropdown] { position: relative !important; top: 60px !important; right: 20px !important; } */ /* Option 2: If it's in a fixed container */ /* .fixed [class*="view"] { position: relative !important; top: 60px !important; } */ /* Option 3: Target by position (fixed elements in top-right) */ /* .fixed.top-0.right-0 [class*="select"], .fixed.top-0.right-0 [class*="dropdown"] { position: relative !important; top: 60px !important; margin-right: 20px !important; } */ /* Option 4: Move it inline with content instead of fixed position */ /* Replace 'ACTUAL_SELECTOR' with the real class name from browser inspection */ /* ACTUAL_SELECTOR { position: static !important; display: inline-block !important; margin-bottom: 20px !important; } */ .code-block > div > div > svg { background-color: #18794e !important; } /* Error 404 Styling */ #error-description > span > div > div { border: 1px solid #18794e !important; } body > div.relative.antialiased.text-gray-500.dark\:text-gray-400 > div.peer-\[\.is-not-custom\]\:lg\:flex.peer-\[\.is-custom\]\:\[\&\>div\:first-child\]\:\!hidden.peer-\[\.is-custom\]\:\[\&\>div\:first-child\]\:sm\:\!hidden.peer-\[\.is-custom\]\:\[\&\>div\:first-child\]\:md\:\!hidden.peer-\[\.is-custom\]\:\[\&\>div\:first-child\]\:lg\:\!hidden.peer-\[\.is-custom\]\:\[\&\>div\:first-child\]\:xl\:\!hidden > div.flex.flex-col.items-center.justify-center.w-full.max-w-lg.overflow-x-hidden.mx-auto.py-48.px-5.text-center.\*\:text-center.gap-y-8.not-found-container > div { margin-top: -5rem; } #error-description > span > div > div > div.relative.rounded-xl.overflow-hidden.flex.justify-center > img { width: 500px; aspect-ratio: 4 / 3; object-fit: cover; /* border: 1px solid #fff; */ } /* Step List Color Icons Styling */ /* #content > div.steps > div > div.absolute.ml-\[-13px\].py-2 > div { background-color: #18794e !important; } */ /* Step List Color Titles */ #content > div.steps.ml-3\.5.mt-10.mb-6 > div > div.w-full.overflow-hidden.pl-8.pr-px > p { color: #2b9a66 !important; } /* View Dropdown */ /* #radix-_R_5slubt9fen9fdb_ */ /* Turn off bg-white in dark mode for multi-view dropdown (PALM THEME BUG) */ .dark .bg-white\/\[0\.95\].multi-view-dropdown-trigger { background-color: transparent !important; background: none !important; } /* Sidebar collapse button - bigger and easier to click */ /* #sidebar button.absolute { min-width: 2.5rem !important; min-height: 2.5rem !important; padding: 0.75rem !important; z-index: 100 !important; } */ /* Override US flag with UK flag in language selector */ /* Hide the original img and use background-image instead */ /* #localization-select-trigger img[alt="US"], #localization-select-item-en img[alt="US"], img[alt="US"][src*="flags/US.svg"] { opacity: 0 !important; position: relative !important; } #localization-select-trigger img[alt="US"]::before, #localization-select-item-en img[alt="US"]::before, img[alt="US"][src*="flags/US.svg"]::before { content: "" !important; position: absolute !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; background-image: url("/snippets/assets/media/images/site/united-kingdom-flag-icon.svg") !important; background-size: cover !important; background-position: center !important; border-radius: 50% !important; opacity: 1 !important; } */ /* Hide the panel on frame mode pages (MINTLIFY SUCKS) */ /* Hide empty table of contents layout only when it's empty */ #table-of-contents-layout:empty, #content-side-layout:has(#table-of-contents-layout:empty) { display: none; } /* DynamicTable: force fixed layout so columnWidths prop values take effect. Mintlify's Tailwind prose resets table-layout to auto — !important required. */ [data-docs-dynamic-table] { table-layout: fixed !important; } /* StyledTable should sit flush inside its own border shell. Mint wraps rendered tables in a scroll container with vertical padding, which creates a false gap above/below the header row. */ [data-docs-styled-table-shell] > div { padding-top: 0 !important; padding-bottom: 0 !important; margin-top: 0 !important; margin-bottom: 0 !important; } /* BorderedBox should own its internal spacing. Trim default block margins on the first/last rendered child so headings and paragraphs do not add a false gap inside the padded shell. */ [data-docs-bordered-box] > :first-child { margin-top: 0 !important; } [data-docs-bordered-box] > :last-child { margin-bottom: 0 !important; } [data-docs-bordered-box][data-accent-bar]::before { content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 4px; background-color: var(--accent-bar-color); border-radius: inherit; border-top-right-radius: 0; border-bottom-right-radius: 0; } /* Frame mode container - 80% of #content-container width, centered */ /* Breaks out of #content padding to center in full #content-container */ .frame-mode-container { width: calc(100% + 96px + 20px); /* 976px */ margin-left: -96px; margin-right: -20px; margin-bottom: 2rem; padding-left: 15%; /* Adjust this for desired content width */ padding-right: 15%; /* Adjust this for desired content width */ box-sizing: border-box; } /* Frame mode container inside hero - already broken out, so reset */ .frame-mode-hero-full .frame-mode-container { width: 100%; margin-left: 0; margin-right: 0; padding-left: 0%; padding-right: 0%; } /* Pagination on frame mode pages ONLY - match container padding */ [data-page-mode='frame'] #pagination { width: calc(100% + 96px + 20px); margin-left: -96px; margin-right: -20px; padding-left: calc((100% + 96px + 20px) * 0.1 + 96px); padding-right: calc((100% + 96px + 20px) * 0.1 + 20px); box-sizing: border-box; } /* Hero full width - breaks out of #content padding to fill #content-container */ .frame-mode-hero-full { width: calc(100% + 96px + 20px); margin-left: -96px; margin-right: -20px; position: relative; } @media (max-width: 1023px) { .frame-mode-container { width: 100%; margin-left: 0; margin-right: 0; padding-left: 1rem; padding-right: 1rem; } [data-page-mode='frame'] #pagination { width: 100%; margin-left: 0; margin-right: 0; padding-left: 1rem; padding-right: 1rem; } .frame-mode-hero-full { width: 100%; margin-left: 0; margin-right: 0; } } #starfield { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 0; } /* Target the card content container */ .frame-mode-hero-full [data-component-part='card-content-container'] { padding-top: 0.5rem; padding-bottom: 0.5rem; padding-left: 1rem; padding-right: 2.5rem; /* Space for arrow icon (0.75rem right + icon width ~1rem + margin) */ } /* Target the arrow icon */ .frame-mode-hero-full #card-link-arrow-icon { top: 0.75rem; right: 0.75rem; } /* #content > div.frame-mode-hero-full > div.frame-mode-container > div > div:nth-child(2) > div > div > div:nth-child(4) > a > div { padding-top: 0.5rem; padding-bottom: 0.5rem; } #content > div.frame-mode-hero-full > div.frame-mode-container > div > div:nth-child(2) > div > div > div:nth-child(4) > a > div > #card-link-arrow-icon { top: 0.75rem; right: 0.75rem; } */ /* ============================================ ACCESSIBILITY — Focus indicators ============================================ */ input:focus-visible, select:focus-visible, textarea:focus-visible, button:focus-visible, a:focus-visible, [tabindex]:focus-visible { outline: 2px solid var(--accent) !important; outline-offset: 2px; } /* ============================================ ACCESSIBILITY — Responsive breakpoints ============================================ */ @media (max-width: 767px) { .frame-mode-hero-full { width: 100%; max-width: 100%; overflow-x: hidden; } } @media (max-width: 480px) { #content { padding-left: 1rem; padding-right: 1rem; } } /* ============================================ UTILITY CLASSES — inline element styling Used where components can't replace inline spans (e.g., inside Mintlify , components) ============================================ */ .lp-inline-flex { display: flex; align-items: center; } .lp-text-muted { color: var(--lp-color-text-secondary); } .lp-text-italic-muted { font-style: italic; color: var(--lp-color-text-secondary); } .lp-inline-flex-gap { display: flex; align-items: center; gap: 0.2rem; } .lp-link-underline { border-bottom: 1.5px solid var(--lp-color-text-primary); color: var(--lp-color-text-primary); padding-bottom: 0.25rem; }
This comprehensive example includes broadcasting controls like audio and video toggles, screenshare capabilities, fullscreen and picture-in-picture modes, along with advanced settings for device selection and error handling. It uses Tailwind CSS for styling, but this can be replaced with any styling solution.

Broadcast

Usage

Here’s how a full Broadcast experience can be built with the primitives:
import { cn } from "@/lib/utils";
import {
  DisableAudioIcon,
  DisableVideoIcon,
  EnableAudioIcon,
  EnableVideoIcon,
  EnterFullscreenIcon,
  ExitFullscreenIcon,
  LoadingIcon,
  OfflineErrorIcon,
  PictureInPictureIcon,
  SettingsIcon,
  StartScreenshareIcon,
  StopIcon,
  StopScreenshareIcon,
} from "@livepeer/react/assets";
import * as Broadcast from "@livepeer/react/broadcast";
import * as Popover from "@radix-ui/react-popover";
import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react";
import React from "react";

import { toast } from "sonner";

export function BroadcastWithControls({
  streamKey,
}: {
  streamKey: string | null;
}) {
  const ingestUrl = getIngest(streamKey);

  return !ingestUrl ? (
    <BroadcastLoading
      title="Invalid stream key"
      description="The stream key provided was invalid. Please check and try again."
    />
  ) : (
    <>
      <Broadcast.Root
        onError={(error) =>
          error?.type === "permissions"
            ? toast.error(
                "You must accept permissions to broadcast. Please try again."
              )
            : null
        }
        aspectRatio={16 / 9}
        ingestUrl={ingestUrl}
      >
        <Broadcast.Container className="w-full h-full overflow-hidden rounded-sm bg-gray-950">
          <Broadcast.Video title="Live stream" className="w-full h-full" />

          <Broadcast.LoadingIndicator className="w-full relative h-full">
            <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
              <LoadingIcon className="w-8 h-8 animate-spin" />
            </div>
            <BroadcastLoading />
          </Broadcast.LoadingIndicator>

          <Broadcast.ErrorIndicator
            matcher="not-permissions"
            className="absolute select-none inset-0 text-center bg-gray-950 flex flex-col items-center justify-center gap-4 duration-1000 data-[visible=true]:animate-in data-[visible=false]:animate-out data-[visible=false]:fade-out-0 data-[visible=true]:fade-in-0"
          >
            <OfflineErrorIcon className="h-[120px] w-full sm:flex hidden" />
            <div className="flex flex-col gap-1">
              <div className="text-2xl font-bold">Broadcast failed</div>
              <div className="text-sm text-gray-100">
                There was an error with broadcasting - it is retrying in the
                background.
              </div>
            </div>
          </Broadcast.ErrorIndicator>

          <Broadcast.Controls className="bg-gradient-to-b gap-1 px-3 md:px-3 py-1.5 flex-col-reverse flex from-black/20 via-80% via-black/30 duration-1000 to-black/60 data-[visible=true]:animate-in data-[visible=false]:animate-out data-[visible=false]:fade-out-0 data-[visible=true]:fade-in-0">
            <div className="flex justify-between gap-4">
              <div className="flex flex-1 items-center gap-3">
                <Broadcast.VideoEnabledTrigger className="w-6 h-6 hover:scale-110 transition flex-shrink-0">
                  <Broadcast.VideoEnabledIndicator asChild matcher={false}>
                    <DisableVideoIcon className="w-full h-full" />
                  </Broadcast.VideoEnabledIndicator>
                  <Broadcast.VideoEnabledIndicator asChild matcher={true}>
                    <EnableVideoIcon className="w-full h-full" />
                  </Broadcast.VideoEnabledIndicator>
                </Broadcast.VideoEnabledTrigger>
                <Broadcast.AudioEnabledTrigger className="w-6 h-6 hover:scale-110 transition flex-shrink-0">
                  <Broadcast.AudioEnabledIndicator asChild matcher={false}>
                    <DisableAudioIcon className="w-full h-full" />
                  </Broadcast.AudioEnabledIndicator>
                  <Broadcast.AudioEnabledIndicator asChild matcher={true}>
                    <EnableAudioIcon className="w-full h-full" />
                  </Broadcast.AudioEnabledIndicator>
                </Broadcast.AudioEnabledTrigger>
              </div>
              <div className="flex sm:flex-1 md:flex-[1.5] justify-end items-center gap-2.5">
                <Broadcast.FullscreenIndicator matcher={false} asChild>
                  <Settings className="w-6 h-6 transition flex-shrink-0" />
                </Broadcast.FullscreenIndicator>

                <Broadcast.ScreenshareTrigger className="w-6 h-6 hover:scale-110 transition flex-shrink-0">
                  <Broadcast.ScreenshareIndicator asChild>
                    <StopScreenshareIcon className="w-full h-full" />
                  </Broadcast.ScreenshareIndicator>

                  <Broadcast.ScreenshareIndicator matcher={false} asChild>
                    <StartScreenshareIcon className="w-full h-full" />
                  </Broadcast.ScreenshareIndicator>
                </Broadcast.ScreenshareTrigger>

                <Broadcast.PictureInPictureTrigger className="w-6 h-6 hover:scale-110 transition flex-shrink-0">
                  <PictureInPictureIcon className="w-full h-full" />
                </Broadcast.PictureInPictureTrigger>

                <Broadcast.FullscreenTrigger className="w-6 h-6 hover:scale-110 transition flex-shrink-0">
                  <Broadcast.FullscreenIndicator asChild>
                    <ExitFullscreenIcon className="w-full h-full" />
                  </Broadcast.FullscreenIndicator>

                  <Broadcast.FullscreenIndicator matcher={false} asChild>
                    <EnterFullscreenIcon className="w-full h-full" />
                  </Broadcast.FullscreenIndicator>
                </Broadcast.FullscreenTrigger>
              </div>
            </div>
            <Broadcast.EnabledIndicator
              matcher={false}
              className="flex flex-1 items-center justify-center"
            >
              <Broadcast.EnabledTrigger className="rounded-md px-4 py-2 bg-black/60 hover:bg-black/70 gap-1 flex items-center justify-center">
                <EnableVideoIcon className="w-7 h-7" />
                <span className="text-sm">Start broadcast</span>
              </Broadcast.EnabledTrigger>
            </Broadcast.EnabledIndicator>
            <Broadcast.EnabledIndicator asChild>
              <Broadcast.EnabledTrigger className="top-1 right-2 absolute flex items-center justify-center gap-1 rounded-md px-4 py-2 bg-white/5 hover:bg-white/10">
                <StopIcon className="w-7 h-7" />
                <span className="text-sm">Stop broadcast</span>
              </Broadcast.EnabledTrigger>
            </Broadcast.EnabledIndicator>
          </Broadcast.Controls>

          <Broadcast.LoadingIndicator asChild matcher={false}>
            <div className="absolute overflow-hidden py-1 px-2 rounded-full top-1 left-1 bg-black/50 flex items-center backdrop-blur">
              <Broadcast.StatusIndicator
                matcher="live"
                className="flex gap-2 items-center"
              >
                <div className="bg-red-500 animate-pulse h-1.5 w-1.5 rounded-full" />
                <span className="text-xs select-none">LIVE</span>
              </Broadcast.StatusIndicator>

              <Broadcast.StatusIndicator
                className="flex gap-2 items-center"
                matcher="pending"
              >
                <div className="bg-white/80 h-1.5 w-1.5 rounded-full animate-pulse" />
                <span className="text-xs select-none">PENDING</span>
              </Broadcast.StatusIndicator>

              <Broadcast.StatusIndicator
                className="flex gap-2 items-center"
                matcher="idle"
              >
                <div className="bg-white/80 h-1.5 w-1.5 rounded-full" />
                <span className="text-xs select-none">IDLE</span>
              </Broadcast.StatusIndicator>
            </div>
          </Broadcast.LoadingIndicator>
        </Broadcast.Container>
      </Broadcast.Root>
    </>
  );
}

export const BroadcastLoading = ({
  title,
  description,
}: {
  title?: React.ReactNode;
  description?: React.ReactNode;
}) => (
  <div className="relative w-full px-3 md:px-3 py-3 gap-3 flex-col-reverse flex aspect-video bg-white/10 overflow-hidden rounded-sm">
    <div className="flex justify-between">
      <div className="flex items-center gap-2">
        <div className="w-6 h-6 animate-pulse bg-white/5 overflow-hidden rounded-lg" />
        <div className="w-16 h-6 md:w-20 md:h-7 animate-pulse bg-white/5 overflow-hidden rounded-lg" />
      </div>

      <div className="flex items-center gap-2">
        <div className="w-6 h-6 animate-pulse bg-white/5 overflow-hidden rounded-lg" />
        <div className="w-6 h-6 animate-pulse bg-white/5 overflow-hidden rounded-lg" />
      </div>
    </div>
    <div className="w-full h-2 animate-pulse bg-white/5 overflow-hidden rounded-lg" />

    {title && (
      <div className="absolute flex flex-col gap-1 inset-10 text-center justify-center items-center">
        <span className="text-white text-lg font-medium">{title}</span>
        {description && (
          <span className="text-sm text-white/80">{description}</span>
        )}
      </div>
    )}
  </div>
);

export const Settings = React.forwardRef(
  (
    { className }: { className?: string },
    ref: React.Ref<HTMLButtonElement> | undefined
  ) => {
    return (
      <Popover.Root>
        <Popover.Trigger ref={ref} asChild>
          <button
            type="button"
            className={className}
            aria-label="Stream settings"
            onClick={(e) => e.stopPropagation()}
          >
            <SettingsIcon />
          </button>
        </Popover.Trigger>
        <Popover.Portal>
          <Popover.Content
            className="w-60 rounded-md bg-black/50 border border-white/50 backdrop-blur-md p-3 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
            side="top"
            alignOffset={-70}
            align="end"
            onClick={(e) => e.stopPropagation()}
          >
            <div className="flex flex-col gap-2">
              <p className="text-white/90 font-medium text-sm mb-1">
                Stream settings
              </p>

              <div className="gap-2 flex-col flex">
                <label
                  className="text-xs text-white/90 font-medium"
                  htmlFor="cameraSource"
                >
                  Camera ('c' to rotate)
                </label>
                <SourceSelectComposed name="cameraSource" type="videoinput" />
              </div>

              <div className="gap-2 flex-col flex">
                <label
                  className="text-xs text-white/90 font-medium"
                  htmlFor="microphoneSource"
                >
                  Microphone ('m' to rotate)
                </label>
                <SourceSelectComposed
                  name="microphoneSource"
                  type="audioinput"
                />
              </div>
            </div>
            <Popover.Close
              className="rounded-full h-5 w-5 inline-flex items-center justify-center absolute top-2.5 right-2.5 outline-none"
              aria-label="Close"
            >
              <XIcon />
            </Popover.Close>
            <Popover.Arrow className="fill-white/50" />
          </Popover.Content>
        </Popover.Portal>
      </Popover.Root>
    );
  }
);

export const SourceSelectComposed = React.forwardRef(
  (
    {
      name,
      type,
      className,
    }: { name: string; type: "audioinput" | "videoinput"; className?: string },
    ref: React.Ref<HTMLButtonElement> | undefined
  ) => (
    <Broadcast.SourceSelect name={name} type={type}>
      {(devices) =>
        devices ? (
          <>
            <Broadcast.SelectTrigger
              ref={ref}
              className={cn(
                "flex w-full items-center overflow-hidden justify-between rounded-sm px-1 outline-1 outline-white/50 text-xs leading-none h-7 gap-1 outline-none disabled:opacity-70 disabled:cursor-not-allowed",
                className
              )}
              aria-label={type === "audioinput" ? "Audio input" : "Video input"}
            >
              <Broadcast.SelectValue
                placeholder={
                  type === "audioinput"
                    ? "Select an audio input"
                    : "Select a video input"
                }
              />
              <Broadcast.SelectIcon>
                <ChevronDownIcon className="h-4 w-4" />
              </Broadcast.SelectIcon>
            </Broadcast.SelectTrigger>
            <Broadcast.SelectPortal>
              <Broadcast.SelectContent className="overflow-hidden bg-black rounded-sm">
                <Broadcast.SelectViewport className="p-1">
                  <Broadcast.SelectGroup>
                    {devices?.map((device) => (
                      <RateSelectItem
                        key={device.deviceId}
                        value={device.deviceId}
                      >
                        {device.friendlyName}
                      </RateSelectItem>
                    ))}
                  </Broadcast.SelectGroup>
                </Broadcast.SelectViewport>
              </Broadcast.SelectContent>
            </Broadcast.SelectPortal>
          </>
        ) : (
          <span>There was an error fetching the available devices.</span>
        )
      }
    </Broadcast.SourceSelect>
  )
);

const RateSelectItem = React.forwardRef<
  HTMLDivElement,
  Broadcast.SelectItemProps
>(({ children, className, ...props }, forwardedRef) => {
  return (
    <Broadcast.SelectItem
      className={cn(
        "text-xs leading-none rounded-sm flex items-center h-7 pr-[35px] pl-[25px] relative select-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-white/20",
        className
      )}
      {...props}
      ref={forwardedRef}
    >
      <Broadcast.SelectItemText>{children}</Broadcast.SelectItemText>
      <Broadcast.SelectItemIndicator className="absolute left-0 w-[25px] inline-flex items-center justify-center">
        <CheckIcon className="w-4 h-4" />
      </Broadcast.SelectItemIndicator>
    </Broadcast.SelectItem>
  );
});
Last modified on March 8, 2026