Skip to content
FrameworkStyle

Customize skins

Learn the public customization surface for packaged Video.js skins and when to eject

Video.js v10 comes with two pre-built skins: Default and Minimal. Start with their public CSS custom properties. When you need to change controls, layout, or interactions, eject the closest skin by copying its code into your project and making it your own.

Basic customization

Set these CSS custom properties directly on the skin:

<VideoSkin
  style={
    {
      '--media-accent-color': '#f5c518',
      '--media-border-radius': '1rem',
    } as React.CSSProperties
  }
>
  <Video src="video.mp4" />
</VideoSkin>
Property Name Description Type Example
--media-accent-color The color of slider fills and accented controls <color> #f5c518
--media-accent-text-color The color of text and icons shown on the accent color <color> black
--media-border-radius The border radius of the media player A valid border-radius value 1rem
--media-scale-unit The base sizing unit for skin spacing and icons <length> 1rem

If you omit --media-accent-text-color, the skins choose a contrasting text color from --media-accent-color using contrast-color(). You only need to set it when you want to override that result.

Set the audio color scheme

The audio skins adapt their main palette for light and dark mode using light-dark(). It uses the inherited color-scheme, so set color-scheme on the player or an ancestor:

:root {
  color-scheme: light dark;
}

The video skins keep their dark control presentation in both color schemes.

Scale with the document font size

Vanilla CSS and Tailwind skins use --media-scale-unit as the base for control spacing and icon sizes. Vanilla CSS skins also use it for font sizes. It defaults to 16px, so player sizing does not change with the document root font size or the app’s Tailwind spacing scale. Set it to 1rem on the player or an ancestor to make the UI scale with the document root font size:

.player {
  --media-scale-unit: 1rem;
}

You can also add your own class names to the skins.

Where packaged customization stops

The four properties above are the public styling controls for packaged skins. Don’t depend on the elements, classnames, or other custom properties inside a packaged skin. They may change between releases.

Packaged video skins also let you replace the poster through the renderPoster prop.

See Poster.

For anything deeper, eject instead of reaching into the packaged skin. There are no packaged-skin options for adding, removing, or rearranging controls, or replacing its built-in feedback and interactions. Seek buttons are a common example: the audio skins include 10-second skip-back and skip-forward buttons, but the video skins ship without them. To add rewind and fast-forward controls to a video skin, eject it and place a SeekButton in the control bar.

Eject a skin

Eject as soon as the packaged skin no longer matches the player you need. Once ejected, removing a control is removing a component, and changing an overlay or interaction is editing code you own.

An ejected skin is app code, so its markup, class names, and styles are yours to change. If none of the packaged skins is a useful starting point, build a custom UI from the player, container, and individual UI components instead.

Soon, we’ll have a CLI that ejects skins in your preferred framework and style. For now, we invite you to try these copy-paste-ready implementations.

To eject a skin:

  1. Choose the closest implementation below.
  2. Copy every file shown for your framework into your project.
  3. Replace the packaged skin with the copied component or player markup, then edit the files you own.

Default video skin

'use client';

import { type ComponentProps, type SVGProps, type ReactElement, type ReactNode, type CSSProperties } from 'react';
import { ErrorDialog as ErrorDialogPrimitive, Container as ContainerPrimitive, Poster as PosterPrimitive, Gesture, Hotkey, SeekIndicator as SeekIndicatorPrimitive, StatusIndicator as StatusIndicatorPrimitive, VolumeIndicator as VolumeIndicatorPrimitive, StatusAnnouncer as StatusAnnouncerPrimitive, BufferingIndicator as BufferingIndicatorPrimitive, Tooltip, MuteButton as MuteButtonPrimitive, VolumeSlider as VolumeSliderPrimitive, VolumePopover as VolumePopoverPrimitive, Menu, Text as TextPrimitive, AudioTrackRadioGroup, CaptionsRadioGroup, PlaybackRateRadioGroup, QualityRadioGroup, AirPlayButton as AirPlayButtonPrimitive, CaptionsButton as CaptionsButtonPrimitive, CastButton as CastButtonPrimitive, FullscreenButton as FullscreenButtonPrimitive, PiPButton as PiPButtonPrimitive, PlayButton as PlayButtonPrimitive, TimeSlider as TimeSliderPrimitive, Slider, Controls, Time } from '@videojs/react';
import { Video } from '@videojs/react/video';
import './player.css';
import { Player } from './player';
import { ChevronIcon as ChevronIconPrimitive, CaptionsOnIcon as CaptionsOnIconPrimitive, CaptionsOffIcon as CaptionsOffIconPrimitive, FullscreenEnterIcon as FullscreenEnterIconPrimitive, FullscreenExitIcon as FullscreenExitIconPrimitive, PipEnterIcon as PipEnterIconPrimitive, PipExitIcon as PipExitIconPrimitive, PlayIcon as PlayIconPrimitive, PauseIcon as PauseIconPrimitive, VolumeHighIcon as VolumeHighIconPrimitive, VolumeLowIcon as VolumeLowIconPrimitive, VolumeOffIcon as VolumeOffIconPrimitive, SpinnerIcon as SpinnerIconPrimitive, CheckIcon as CheckIconPrimitive, SpeechIcon as SpeechIconPrimitive, SpeedIcon as SpeedIconPrimitive, SwitchesIcon as SwitchesIconPrimitive, GearIcon as GearIconPrimitive, AirPlayEnterIcon as AirPlayEnterIconPrimitive, AirPlayExitIcon as AirPlayExitIconPrimitive, CastEnterIcon as CastEnterIconPrimitive, CastExitIcon as CastExitIconPrimitive, RestartIcon as RestartIconPrimitive } from '@videojs/react/icons';
import { type VolumeSliderProps as CoreVolumeSliderProps, type SliderPreviewOverflow } from '@videojs/core';
import { audioText, captionsText, speedText, qualityText, settingsText } from '@videojs/core/i18n/text/menu';
import { type Poster } from '@videojs/react';

type ContainerProps = ContainerPrimitive.Props;

function Container({ children, className, ...props }: ContainerProps) {
  return (
    <ContainerPrimitive className={`media-skin media-container ${className ?? ''}`} {...props}>
      {children}
    </ContainerPrimitive>
  );
}

interface PosterProps extends Omit<PosterPrimitive.Props, 'children' | 'render'> {
  children?: PosterPrimitive.Props['render'];
}

function Poster({ children, className, src, ...props }: PosterProps = {}) {
  return (
    <PosterPrimitive
      render={children}
      className={(state) => `media-poster ${resolveClassName(className, state)}`}
      src={src}
      {...props}
    />
  );
}

interface DefaultVideoSkinProps extends Omit<NonNullable<ComponentProps<typeof Container>>, 'children'> {
  src: string;
  poster?: string | undefined;
  renderPoster?: NonNullable<ComponentProps<typeof Poster>>['children'];
}

type BufferingIndicatorProps = Omit<BufferingIndicatorPrimitive.Props, 'children'>;

function BufferingIndicator({ className, ...props }: BufferingIndicatorProps = {}) {
  return (
    <BufferingIndicatorPrimitive
      className={(state) => `media-buffering-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <SpinnerIconPrimitive className={'media-buffering-indicator-spinner-icon'} />
    </BufferingIndicatorPrimitive>
  );
}

type ButtonProps = ComponentProps<'button'>;

interface ButtonTooltipProps extends Tooltip.RootProps {
  children: ReactElement;
  label?: ReactNode;
}

function ButtonTooltip({ children, label, ...props }: ButtonTooltipProps) {
  return (
    <Tooltip.Root {...props}>
      <Tooltip.Trigger render={children} />
      <Tooltip.Popup
        className={'media-popup media-popup-safe-area media-popup-transition media-popup-surface media-tooltip'}
      >
        {label ?? <Tooltip.Label />}
        {!label && <Tooltip.Shortcut className={'media-tooltip-shortcut'} />}
      </Tooltip.Popup>
    </Tooltip.Root>
  );
}

type PlayButtonProps = Omit<PlayButtonPrimitive.Props, 'children'>;

function PlayButton({ className, ...props }: PlayButtonProps = {}) {
  return (
    <PlayButtonPrimitive
      render={<Button />}
      className={(state) => `media-play-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <RestartIconPrimitive className={'media-button-icon media-play-button-restart-icon'} />
      <PlayIconPrimitive className={'media-button-icon media-play-button-play-icon'} />
      <PauseIconPrimitive className={'media-button-icon media-play-button-pause-icon'} />
    </PlayButtonPrimitive>
  );
}

interface VolumePopoverProps extends Omit<VolumePopoverPrimitive.RootProps, 'children'> {
  className?: ClassValue;
  orientation?: CoreVolumeSliderProps['orientation'];
  showTooltip?: boolean;
}

type MuteButtonProps = Omit<MuteButtonPrimitive.Props, 'children'>;

function MuteButton({ className, ...props }: MuteButtonProps = {}) {
  return (
    <MuteButtonPrimitive
      render={<Button />}
      className={(state) => `media-mute-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <VolumeOffIconPrimitive className={'media-button-icon media-mute-button-off-icon'} />
      <VolumeLowIconPrimitive className={'media-button-icon media-mute-button-low-icon'} />
      <VolumeHighIconPrimitive className={'media-button-icon media-mute-button-high-icon'} />
    </MuteButtonPrimitive>
  );
}

type VolumeSliderProps = Omit<VolumeSliderPrimitive.RootProps, 'children'>;

function VolumeSlider({ className, ...props }: VolumeSliderProps = {}) {
  return (
    <VolumeSliderPrimitive.Root
      className={(state) => `media-slider media-volume-slider ${resolveClassName(className, state)}`}
      thumbAlignment="edge"
      {...props}
    >
      <VolumeSliderPrimitive.Track render={<SliderTrack />}>
        <VolumeSliderPrimitive.Fill render={<SliderFill />} />
      </VolumeSliderPrimitive.Track>
      <VolumeSliderPrimitive.Thumb render={<SliderThumb />} className={'media-volume-slider-thumb'} />
    </VolumeSliderPrimitive.Root>
  );
}

interface TimeSliderProps extends Omit<TimeSliderPrimitive.RootProps, 'children'> {
  previewOverflow?: SliderPreviewOverflow | undefined;
}

function TimeSlider({ className, previewOverflow = 'visible', ...props }: TimeSliderProps = {}) {
  return (
    <TimeSliderPrimitive.Root
      className={(state) => `media-slider media-time-slider ${resolveClassName(className, state)}`}
      {...props}
    >
      <TimeSliderPrimitive.Chapters
        className={'media-time-slider-chapters'}
        renderChapter={(props) => (
          <div className={'media-time-slider-chapter'} {...props}>
            <TimeSliderPrimitive.Track render={<SliderTrack />} className={'media-time-slider-chapter-track'}>
              <TimeSliderPrimitive.Buffer render={<SliderBuffer />} />
              <TimeSliderPrimitive.Fill render={<SliderFill />} />
            </TimeSliderPrimitive.Track>
          </div>
        )}
      ></TimeSliderPrimitive.Chapters>
      <TimeSliderPrimitive.Thumb render={<SliderThumb />} className={'media-time-slider-thumb'} />
      <TimeSliderPrimitive.Preview className={'media-slider-preview'} overflow={previewOverflow}>
        <div className={'media-slider-preview-content media-popup-surface media-slider-thumbnail'}>
          <Slider.Thumbnail className={'media-slider-thumbnail-image'} />
          <SpinnerIconPrimitive className={'media-slider-thumbnail-spinner-icon'} />
        </div>
        <div className={'media-slider-preview-content media-time-slider-preview-content'}>
          <TimeSliderPrimitive.ChapterTitle className={'media-time-slider-chapter-title'} />
          <TimeSliderPrimitive.Value className={'media-time-slider-value'} type="pointer" />
        </div>
      </TimeSliderPrimitive.Preview>
    </TimeSliderPrimitive.Root>
  );
}

type CaptionsButtonProps = Omit<CaptionsButtonPrimitive.Props, 'children'>;

function CaptionsButton({ className, ...props }: CaptionsButtonProps = {}) {
  return (
    <CaptionsButtonPrimitive
      render={<Button />}
      className={(state) => `media-captions-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <CaptionsOffIconPrimitive className={'media-button-icon media-captions-button-off-icon'} />
      <CaptionsOnIconPrimitive className={'media-button-icon media-captions-button-on-icon'} />
    </CaptionsButtonPrimitive>
  );
}

interface SettingsMenuProps extends Menu.RootProps {
  className?: ClassValue;
}

function SettingsMenu({ children, className, ...props }: SettingsMenuProps) {
  return (
    <Menu.Root side="top" align="center" {...props}>
      <ButtonTooltip label={<TextPrimitive token={settingsText.key}>{settingsText.text}</TextPrimitive>} side="top">
        <Menu.Trigger
          render={<Button />}
          className={(state) => `media-settings-menu-trigger ${resolveClassName(className, state)}`}
        >
          <GearIconPrimitive className={'media-button-icon media-settings-menu-trigger-icon'} />
          <TextPrimitive className={'media-settings-menu-trigger-label'} token={settingsText.key}>
            {settingsText.text}
          </TextPrimitive>
        </Menu.Trigger>
      </ButtonTooltip>
      <Menu.Popup
        keepMounted
        className={'media-popup media-popup-surface media-menu-popup media-menu-resizable-popup'}
      >
        <Menu.Content className={'media-menu-content'}>{children}</Menu.Content>
      </Menu.Popup>
    </Menu.Root>
  );
}

type VideoSettingsMenuProps = Omit<NonNullable<ComponentProps<typeof SettingsMenu>>, 'children'>;

type QualityMenuProps = Omit<Menu.RootProps, 'children'>;

interface MenuChevronProps {
  back?: boolean;
  className?: ClassValue;
}

function MenuChevron({ back = false, className }: MenuChevronProps = {}) {
  return (
    <ChevronIconPrimitive className={`${back ? 'media-menu-back-chevron' : 'media-menu-forward-chevron'} ${className ?? ''}`} />
  );
}

type RadioItemProps = Menu.RadioItemProps;

function RadioItem({ children, className, ...props }: RadioItemProps) {
  return (
    <Menu.RadioItem className={(state) => `media-menu-radio-item ${resolveClassName(className, state)}`} {...props}>
      {children}
      <Menu.ItemIndicator forceMount className={'media-menu-item-indicator'}>
        <CheckIconPrimitive className={'media-menu-radio-item-icon'} />
      </Menu.ItemIndicator>
    </Menu.RadioItem>
  );
}

function QualityMenu({ ...props }: QualityMenuProps = {}) {
  return (
    <Menu.Root {...props}>
      <QualityRadioGroup.Root>
        <Menu.Trigger className={'media-menu-trigger-item'}>
          <SwitchesIconPrimitive className={'media-menu-trigger-item-icon'} />
          <TextPrimitive token={qualityText.key}>{qualityText.text}</TextPrimitive>
          <span className={'media-menu-hint'}>
            <QualityRadioGroup.Value className={'media-menu-hint-label'} />
            <MenuChevron />
          </span>
        </Menu.Trigger>
        <Menu.Content className={'media-menu-content'}>
          <Menu.Item className={'media-menu-back-item'}>
            <MenuChevron back />
            <TextPrimitive token={qualityText.key}>{qualityText.text}</TextPrimitive>
          </Menu.Item>
          <Menu.Separator className={'media-menu-separator'} />
          <QualityRadioGroup.Options
            className={'media-menu-radio-group'}
            renderItem={(props, item) => (
              <RadioItem {...props}>
                <span>
                  <span>{item.label}</span>
                  {item.tier ? <sup className={'media-menu-tier'}>{item.tier}</sup> : null}
                </span>
                {item.badge ? <span className={'media-menu-badge'}>{item.badge}</span> : null}
              </RadioItem>
            )}
          ></QualityRadioGroup.Options>
        </Menu.Content>
      </QualityRadioGroup.Root>
    </Menu.Root>
  );
}

type AudioTrackMenuProps = Omit<Menu.RootProps, 'children'>;

function AudioTrackMenu({ ...props }: AudioTrackMenuProps = {}) {
  return (
    <Menu.Root {...props}>
      <AudioTrackRadioGroup.Root>
        <Menu.Trigger className={'media-menu-trigger-item'}>
          <SpeechIconPrimitive className={'media-menu-trigger-item-icon'} />
          <TextPrimitive token={audioText.key}>{audioText.text}</TextPrimitive>
          <span className={'media-menu-hint'}>
            <AudioTrackRadioGroup.Value className={'media-menu-hint-label'} />
            <MenuChevron />
          </span>
        </Menu.Trigger>
        <Menu.Content className={'media-menu-content'}>
          <Menu.Item className={'media-menu-back-item'}>
            <MenuChevron back />
            <TextPrimitive token={audioText.key}>{audioText.text}</TextPrimitive>
          </Menu.Item>
          <Menu.Separator className={'media-menu-separator'} />
          <AudioTrackRadioGroup.Options
            className={'media-menu-radio-group'}
            renderItem={(props, item) => (
              <RadioItem {...props}>
                <span>{item.label}</span>
              </RadioItem>
            )}
          ></AudioTrackRadioGroup.Options>
        </Menu.Content>
      </AudioTrackRadioGroup.Root>
    </Menu.Root>
  );
}

type PlaybackRateSubmenuProps = Omit<Menu.RootProps, 'children'>;

function PlaybackRateSubmenu({ ...props }: PlaybackRateSubmenuProps = {}) {
  return (
    <Menu.Root {...props}>
      <PlaybackRateRadioGroup.Root>
        <Menu.Trigger className={'media-menu-trigger-item'}>
          <SpeedIconPrimitive className={'media-menu-trigger-item-icon'} />
          <TextPrimitive token={speedText.key}>{speedText.text}</TextPrimitive>
          <span className={'media-menu-hint'}>
            <PlaybackRateRadioGroup.Value className={'media-menu-hint-label'} />
            <MenuChevron />
          </span>
        </Menu.Trigger>
        <Menu.Content className={'media-menu-content'}>
          <Menu.Item className={'media-menu-back-item'}>
            <MenuChevron back />
            <TextPrimitive token={speedText.key}>{speedText.text}</TextPrimitive>
          </Menu.Item>
          <Menu.Separator className={'media-menu-separator'} />
          <PlaybackRateRadioGroup.Options
            className={'media-menu-radio-group'}
            renderItem={(props, item) => (
              <RadioItem {...props}>
                <span>{item.label}</span>
              </RadioItem>
            )}
          ></PlaybackRateRadioGroup.Options>
        </Menu.Content>
      </PlaybackRateRadioGroup.Root>
    </Menu.Root>
  );
}

type CaptionsSubmenuProps = Omit<Menu.RootProps, 'children'>;

function CaptionsSubmenu({ ...props }: CaptionsSubmenuProps = {}) {
  return (
    <Menu.Root {...props}>
      <CaptionsRadioGroup.Root>
        <Menu.Trigger className={'media-menu-trigger-item'}>
          <CaptionsOffIconPrimitive className={'media-menu-trigger-item-icon'} />
          <TextPrimitive token={captionsText.key}>{captionsText.text}</TextPrimitive>
          <span className={'media-menu-hint'}>
            <CaptionsRadioGroup.Value className={'media-menu-hint-label'} />
            <MenuChevron />
          </span>
        </Menu.Trigger>
        <Menu.Content className={'media-menu-content'}>
          <Menu.Item className={'media-menu-back-item'}>
            <MenuChevron back />
            <TextPrimitive token={captionsText.key}>{captionsText.text}</TextPrimitive>
          </Menu.Item>
          <Menu.Separator className={'media-menu-separator'} />
          <CaptionsRadioGroup.Options
            className={'media-menu-radio-group'}
            renderItem={(props, item) => (
              <RadioItem {...props}>
                <span>{item.label}</span>
              </RadioItem>
            )}
          ></CaptionsRadioGroup.Options>
        </Menu.Content>
      </CaptionsRadioGroup.Root>
    </Menu.Root>
  );
}

function VideoSettingsMenu(props: VideoSettingsMenuProps = {}) {
  return (
    <SettingsMenu {...props}>
      <QualityMenu />
      <AudioTrackMenu />
      <PlaybackRateSubmenu />
      <CaptionsSubmenu />
    </SettingsMenu>
  );
}

type CastButtonProps = Omit<CastButtonPrimitive.Props, 'children'>;

function CastButton({ className, ...props }: CastButtonProps = {}) {
  return (
    <CastButtonPrimitive
      render={<Button />}
      className={(state) => `media-cast-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <CastEnterIconPrimitive className={'media-button-icon media-cast-button-enter-icon'} />
      <CastExitIconPrimitive className={'media-button-icon media-cast-button-exit-icon'} />
    </CastButtonPrimitive>
  );
}

type AirPlayButtonProps = Omit<AirPlayButtonPrimitive.Props, 'children'>;

function AirPlayButton({ className, ...props }: AirPlayButtonProps = {}) {
  return (
    <AirPlayButtonPrimitive
      render={<Button />}
      className={(state) => `media-airplay-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <AirPlayEnterIconPrimitive className={'media-button-icon media-airplay-button-enter-icon'} />
      <AirPlayExitIconPrimitive className={'media-button-icon media-airplay-button-exit-icon'} />
    </AirPlayButtonPrimitive>
  );
}

type PiPButtonProps = Omit<PiPButtonPrimitive.Props, 'children'>;

function PiPButton({ className, ...props }: PiPButtonProps = {}) {
  return (
    <PiPButtonPrimitive
      render={<Button />}
      className={(state) => `media-pip-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <PipEnterIconPrimitive className={'media-button-icon media-pip-button-enter-icon'} />
      <PipExitIconPrimitive className={'media-button-icon media-pip-button-exit-icon'} />
    </PiPButtonPrimitive>
  );
}

type FullscreenButtonProps = Omit<FullscreenButtonPrimitive.Props, 'children'>;

function FullscreenButton({ className, ...props }: FullscreenButtonProps = {}) {
  return (
    <FullscreenButtonPrimitive
      render={<Button />}
      className={(state) => `media-fullscreen-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <FullscreenEnterIconPrimitive className={'media-button-icon media-fullscreen-button-enter-icon'} />
      <FullscreenExitIconPrimitive className={'media-button-icon media-fullscreen-button-exit-icon'} />
    </FullscreenButtonPrimitive>
  );
}

function DefaultVideoControls() {
  return (
    <Controls.Root>
      <Controls.Backdrop className={'video-controls-backdrop'} />
      <Controls.Content className={'video-controls video-controls-content'}>
        <Tooltip.Provider>
          <Controls.Group className={'video-controls-primary'}>
            <ButtonTooltip side="top">
              <PlayButton />
            </ButtonTooltip>
            <VolumePopover className={'video-controls-volume-button'} />

            <Controls.Group className={'video-time-slider-group'}>
              <Time.Value className={'media-time-value video-time-value'} type="current" />
              <TimeSlider />
              <Time.Value className={'media-time-toggle video-time-value'} type="remaining" toggle />
            </Controls.Group>

            <ButtonTooltip side="top">
              <CaptionsButton className={'video-controls-captions-button'} />
            </ButtonTooltip>
            <VideoSettingsMenu className={'video-controls-settings-button'} />
          </Controls.Group>

          <Controls.Group className={'video-controls-secondary'}>
            <ButtonTooltip side="top">
              <CastButton />
            </ButtonTooltip>
            <ButtonTooltip side="top">
              <AirPlayButton />
            </ButtonTooltip>
            <ButtonTooltip side="top">
              <PiPButton />
            </ButtonTooltip>
            <ButtonTooltip side="top">
              <FullscreenButton />
            </ButtonTooltip>
          </Controls.Group>
        </Tooltip.Provider>
      </Controls.Content>
    </Controls.Root>
  );
}

interface VideoHotkeysProps {
  disabled?: boolean | undefined;
}

interface PlaybackHotkeysProps {
  disabled?: boolean | undefined;
}

function PlaybackHotkeys({ disabled = false }: PlaybackHotkeysProps = {}) {
  return (
    <>
      <Hotkey disabled={disabled} keys="Space" action="togglePaused" />
      <Hotkey disabled={disabled} keys="k" action="togglePaused" />
      <Hotkey disabled={disabled} keys="m" action="toggleMuted" />
      <Hotkey disabled={disabled} keys="ArrowRight" action="seekStep" />
      <Hotkey disabled={disabled} keys="ArrowLeft" action="seekStep" />
      <Hotkey disabled={disabled} keys="l" action="seekStep" />
      <Hotkey disabled={disabled} keys="j" action="seekStep" />
      <Hotkey disabled={disabled} keys="ArrowUp" action="volumeStep" />
      <Hotkey disabled={disabled} keys="ArrowDown" action="volumeStep" />
      <Hotkey disabled={disabled} keys="0-9" action="seekToPercent" />
      <Hotkey disabled={disabled} keys="Home" action="seekToPercent" value={0} />
      <Hotkey disabled={disabled} keys="End" action="seekToPercent" value={100} />
      <Hotkey disabled={disabled} keys=">" action="speedUp" />
      <Hotkey disabled={disabled} keys="<" action="speedDown" />
    </>
  );
}

function VideoHotkeys({ disabled = false }: VideoHotkeysProps = {}) {
  return (
    <>
      <PlaybackHotkeys disabled={disabled} />
      <Hotkey disabled={disabled} keys="f" action="toggleFullscreen" />
      <Hotkey disabled={disabled} keys="c" action="toggleSubtitles" />
      <Hotkey disabled={disabled} keys="i" action="togglePictureInPicture" />
    </>
  );
}

interface VideoGesturesProps {
  disabled?: boolean | undefined;
}

function VideoGestures({ disabled = false }: VideoGesturesProps = {}) {
  return (
    <>
      <Gesture disabled={disabled} type="tap" action="togglePaused" pointer="mouse" region="center" />
      <Gesture disabled={disabled} type="tap" action="toggleControls" pointer="touch" />
      <Gesture disabled={disabled} type="doubletap" action="seekStep" region="left" />
      <Gesture disabled={disabled} type="doubletap" action="toggleFullscreen" region="center" />
      <Gesture disabled={disabled} type="doubletap" action="seekStep" region="right" />
    </>
  );
}

type VideoStatusIndicatorsProps = Omit<ComponentProps<'div'>, 'children'>;

type StatusAnnouncerProps = Omit<StatusAnnouncerPrimitive.Props, 'children'>;

function StatusAnnouncer({ className, ...props }: StatusAnnouncerProps = {}) {
  return (
    <StatusAnnouncerPrimitive
      className={(state) => `media-status-announcer ${resolveClassName(className, state)}`}
      {...props}
    />
  );
}

type VolumeIndicatorProps = Omit<VolumeIndicatorPrimitive.RootProps, 'children'>;

function VolumeIndicator({ className, ...props }: VolumeIndicatorProps = {}) {
  return (
    <VolumeIndicatorPrimitive.Root
      className={(state) => `media-indicator media-volume-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <VolumeIndicatorPrimitive.Fill className={'media-indicator-content media-volume-indicator-fill'}>
        <VolumeHighIconPrimitive className={'media-volume-indicator-high-icon'} />
        <VolumeLowIconPrimitive className={'media-volume-indicator-low-icon'} />
        <VolumeOffIconPrimitive className={'media-volume-indicator-off-icon'} />
        <VolumeIndicatorPrimitive.Value className={'media-volume-indicator-value'} />
      </VolumeIndicatorPrimitive.Fill>
    </VolumeIndicatorPrimitive.Root>
  );
}

type StatusIndicatorProps = Omit<StatusIndicatorPrimitive.RootProps, 'children'>;

const TOP_STATUS_ACTIONS = ['toggleSubtitles', 'toggleFullscreen', 'togglePictureInPicture'] as const;

function StatusIndicator({ className, ...props }: StatusIndicatorProps = {}) {
  return (
    <StatusIndicatorPrimitive.Root
      actions={TOP_STATUS_ACTIONS}
      className={(state) => `media-indicator media-status-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <div className={'media-indicator-content media-status-indicator-content'}>
        <CaptionsOnIconPrimitive className={'media-status-indicator-captions-on-icon'} />
        <CaptionsOffIconPrimitive className={'media-status-indicator-captions-off-icon'} />
        <FullscreenEnterIconPrimitive className={'media-status-indicator-fullscreen-enter-icon'} />
        <FullscreenExitIconPrimitive className={'media-status-indicator-fullscreen-exit-icon'} />
        <PipEnterIconPrimitive className={'media-status-indicator-pip-enter-icon'} />
        <PipExitIconPrimitive className={'media-status-indicator-pip-exit-icon'} />
        <StatusIndicatorPrimitive.Value className={'media-status-indicator-value'} />
      </div>
    </StatusIndicatorPrimitive.Root>
  );
}

type SeekIndicatorProps = Omit<SeekIndicatorPrimitive.RootProps, 'children'>;

function SeekIndicator({ className, ...props }: SeekIndicatorProps = {}) {
  return (
    <SeekIndicatorPrimitive.Root
      className={(state) => `media-seek-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <ChevronIconPrimitive className={'media-seek-indicator-icon'} />
      <SeekIndicatorPrimitive.Value className={'media-seek-indicator-value'} />
    </SeekIndicatorPrimitive.Root>
  );
}

type PlaybackStatusIndicatorProps = Omit<StatusIndicatorPrimitive.RootProps, 'children'>;

const PLAYBACK_STATUS_ACTIONS = ['togglePaused'] as const;

function PlaybackStatusIndicator({ className, ...props }: PlaybackStatusIndicatorProps = {}) {
  return (
    <StatusIndicatorPrimitive.Root
      actions={PLAYBACK_STATUS_ACTIONS}
      className={(state) => `media-playback-status-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <PlayIconPrimitive className={'media-playback-status-indicator-play-icon'} />
      <PauseIconPrimitive className={'media-playback-status-indicator-pause-icon'} />
    </StatusIndicatorPrimitive.Root>
  );
}

function VideoStatusIndicators({ className, ...props }: VideoStatusIndicatorsProps = {}) {
  return (
    <>
      <StatusAnnouncer />
      <div className={`video-status-indicators ${className ?? ''}`} {...props}>
        <VolumeIndicator />
        <StatusIndicator />
        <SeekIndicator />
        <PlaybackStatusIndicator />
      </div>
    </>
  );
}

function DefaultVideoSkin({ children, className, renderPoster, ...props }: DefaultVideoSkinProps = {}) {
  return (
    <Container className={`video-skin ${className ?? ''}`} data-theme="default" data-preset="video" {...props}>
      {children}
      <Poster>{renderPoster}</Poster>
      <BufferingIndicator />
      <ErrorDialog />

      <DefaultVideoControls />

      <VideoHotkeys />
      <VideoGestures />
      <VideoStatusIndicators />
    </Container>
  );
}

const Skin = DefaultVideoSkin;

// ================================================================
// Player
// ================================================================

export interface VideoPlayerProps {
  children?: ReactNode;
  style?: CSSProperties;
  className?: string;
  renderPoster?: Poster.Props['render'];
}

export function VideoSkin(props: VideoSkinProps) {
  return <Skin {...props} />;
}

// ================================================================
// Components
// ================================================================

function Button({ className, ...props }: ButtonProps) {
  return <button className={`media-button ${className ?? ''}`} {...props} />;
}

type SliderTrackProps = ComponentProps<'div'>;

function SliderTrack({ className, ...props }: SliderTrackProps) {
  return <div className={`media-slider-track ${className ?? ''}`} {...props} />;
}

type SliderFillProps = ComponentProps<'div'>;

function SliderFill({ className, ...props }: SliderFillProps) {
  return <div className={`media-slider-fill ${className ?? ''}`} {...props} />;
}

type SliderThumbProps = ComponentProps<'div'>;

function SliderThumb({ className, ...props }: SliderThumbProps) {
  return <div className={`media-slider-thumb ${className ?? ''}`} {...props} />;
}

function VolumePopover({
  className,
  showTooltip = false,
  side = 'top',
  orientation = 'vertical',
  ...props
}: VolumePopoverProps = {}) {
  return (
    <VolumePopoverPrimitive.Root openOnHover delay={200} closeDelay={100} side={side} {...props}>
      <ButtonTooltip delay={0} disabled={!showTooltip} sticky side="top">
        <VolumePopoverPrimitive.Trigger render={<MuteButton className={`${className ?? ''}`} />} />
      </ButtonTooltip>
      <VolumePopoverPrimitive.Popup
        className={'media-popup media-popup-safe-area media-popup-transition media-popup-surface media-volume-popover'}
      >
        <VolumeSlider orientation={orientation} />
      </VolumePopoverPrimitive.Popup>
    </VolumePopoverPrimitive.Root>
  );
}

type SliderBufferProps = ComponentProps<'div'>;

function SliderBuffer({ className, ...props }: SliderBufferProps) {
  return <div className={`media-slider-buffer ${className ?? ''}`} {...props} />;
}

// ================================================================
// Error Dialog
// ================================================================

function ErrorDialog() {
  return (
    <ErrorDialogPrimitive.Root>
      <ErrorDialogPrimitive.Backdrop className={'media-dialog-backdrop'} />
      <ErrorDialogPrimitive.Popup className={'media-dialog-popup'}>
        <div className={'media-dialog-content'}>
          <ErrorDialogPrimitive.Title className={'media-dialog-title'} />
          <ErrorDialogPrimitive.Description className={'media-dialog-description'} />
        </div>
        <div className={'media-dialog-actions'}>
          <ErrorDialogPrimitive.Close render={<Button />} className={'media-dialog-close'} />
        </div>
      </ErrorDialogPrimitive.Popup>
    </ErrorDialogPrimitive.Root>
  );
}

Default audio skin

'use client';

import { StatusAnnouncer as StatusAnnouncerPrimitive, Container as ContainerPrimitive, ErrorDialog, Hotkey, Tooltip, MuteButton as MuteButtonPrimitive, VolumeSlider as VolumeSliderPrimitive, VolumePopover as VolumePopoverPrimitive, PlayButton as PlayButtonPrimitive, BufferingIndicator as BufferingIndicatorPrimitive, Menu, PlaybackRateButton as PlaybackRateButtonPrimitive, PlaybackRateRadioGroup, TimeSlider, SeekButton as SeekButtonPrimitive, Controls, Time } from '@videojs/react';
import { Audio } from '@videojs/react/audio';
import './player.css';
import { Player } from './player';
import { type ComponentProps, type ReactElement, type ReactNode, type SVGProps, type CSSProperties } from 'react';
import { VolumeOffIcon as VolumeOffIconPrimitive, VolumeLowIcon as VolumeLowIconPrimitive, VolumeHighIcon as VolumeHighIconPrimitive, RestartIcon as RestartIconPrimitive, PlayIcon as PlayIconPrimitive, PauseIcon as PauseIconPrimitive, SpinnerIcon as SpinnerIconPrimitive, CheckIcon as CheckIconPrimitive, SeekIcon as SeekIconPrimitive } from '@videojs/react/icons';
import { type VolumeSliderProps as CoreVolumeSliderProps, type SliderPreviewOverflow } from '@videojs/core';
import { type Poster } from '@videojs/react';

type ContainerProps = ContainerPrimitive.Props;

function Container({ children, className, ...props }: ContainerProps) {
  return (
    <ContainerPrimitive className={`media-skin media-container ${className ?? ''}`} {...props}>
      {children}
    </ContainerPrimitive>
  );
}

interface DefaultAudioSkinProps extends Omit<NonNullable<ComponentProps<typeof Container>>, 'children'> {
  src: string;
}

type ButtonProps = ComponentProps<'button'>;

function AudioErrorDialog() {
  return (
    <ErrorDialog.Root>
      <ErrorDialog.Backdrop className={'audio-dialog-backdrop'} />
      <ErrorDialog.Popup className={'audio-dialog-popup'}>
        <div className={'audio-dialog-content'}>
          <ErrorDialog.Title className={'audio-dialog-title'} />
          <ErrorDialog.Description className={'audio-dialog-description'} />
        </div>
        <div className={'audio-dialog-actions'}>
          <ErrorDialog.Close render={<Button />} className={'audio-dialog-close'} />
        </div>
      </ErrorDialog.Popup>
    </ErrorDialog.Root>
  );
}

type AudioPlayButtonProps = Omit<ComponentProps<'div'>, 'children'>;

type BufferingIndicatorProps = Omit<BufferingIndicatorPrimitive.Props, 'children'>;

function BufferingIndicator({ className, ...props }: BufferingIndicatorProps = {}) {
  return (
    <BufferingIndicatorPrimitive
      className={(state) => `media-buffering-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <SpinnerIconPrimitive className={'media-buffering-indicator-spinner-icon'} />
    </BufferingIndicatorPrimitive>
  );
}

interface ButtonTooltipProps extends Tooltip.RootProps {
  children: ReactElement;
  label?: ReactNode;
}

function ButtonTooltip({ children, label, ...props }: ButtonTooltipProps) {
  return (
    <Tooltip.Root {...props}>
      <Tooltip.Trigger render={children} />
      <Tooltip.Popup
        className={'media-popup media-popup-safe-area media-popup-transition media-popup-surface media-tooltip'}
      >
        {label ?? <Tooltip.Label />}
        {!label && <Tooltip.Shortcut className={'media-tooltip-shortcut'} />}
      </Tooltip.Popup>
    </Tooltip.Root>
  );
}

type PlayButtonProps = Omit<PlayButtonPrimitive.Props, 'children'>;

function PlayButton({ className, ...props }: PlayButtonProps = {}) {
  return (
    <PlayButtonPrimitive
      render={<Button />}
      className={(state) => `media-play-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <RestartIconPrimitive className={'media-button-icon media-play-button-restart-icon'} />
      <PlayIconPrimitive className={'media-button-icon media-play-button-play-icon'} />
      <PauseIconPrimitive className={'media-button-icon media-play-button-pause-icon'} />
    </PlayButtonPrimitive>
  );
}

function AudioPlayButton({ className, ...props }: AudioPlayButtonProps = {}) {
  return (
    <div className={`audio-play-button ${className ?? ''}`} {...props}>
      <BufferingIndicator className={'audio-play-button-buffering-indicator'} />
      <ButtonTooltip boundary="viewport" side="top">
        <PlayButton />
      </ButtonTooltip>
    </div>
  );
}

type SeekButtonProps = Omit<SeekButtonPrimitive.Props, 'children'>;

function SeekButton({ className, seconds = 10, ...props }: SeekButtonProps = {}) {
  return (
    <SeekButtonPrimitive
      render={<Button />}
      className={(state) => `media-seek-button ${resolveClassName(className, state)}`}
      seconds={seconds}
      {...props}
    >
      <div className={'media-seek-button-content'}>
        <SeekIconPrimitive className={`media-button-icon ${seconds < 0 && 'media-seek-button-backward-icon'}`} />
        <span
          className={`media-seek-button-label ${seconds < 0 ? 'media-seek-button-backward-label' : 'media-seek-button-forward-label'}`}
        >
          {Math.abs(seconds)}
        </span>
      </div>
    </SeekButtonPrimitive>
  );
}

interface AudioTimeSliderProps extends Omit<TimeSlider.RootProps, 'children'> {
  previewOverflow?: SliderPreviewOverflow | undefined;
}

function AudioTimeSlider({ className, previewOverflow = 'clamp', ...props }: AudioTimeSliderProps = {}) {
  return (
    <TimeSlider.Root
      className={(state) => `media-slider audio-time-slider ${resolveClassName(className, state)}`}
      {...props}
    >
      <TimeSlider.Track render={<SliderTrack />}>
        <TimeSlider.Buffer render={<SliderBuffer />} />
        <TimeSlider.Fill render={<SliderFill />} />
      </TimeSlider.Track>
      <TimeSlider.Thumb render={<SliderThumb />} className={'audio-time-slider-thumb'} />
      <TimeSlider.Preview className={'media-slider-preview'} overflow={previewOverflow}>
        <div
          className={'media-slider-preview-content media-popup-surface media-tooltip audio-time-slider-preview-content'}
        >
          <TimeSlider.Value className={'audio-time-slider-value'} type="pointer" />
        </div>
      </TimeSlider.Preview>
    </TimeSlider.Root>
  );
}

interface AudioSettingsMenuProps extends Omit<MenuPrimitive.RootProps, 'children'> {
  className?: MenuPrimitive.TriggerProps['className'];
}

type PlaybackRateButtonProps = Omit<PlaybackRateButtonPrimitive.Props, 'children'>;

function PlaybackRateButton({ className, ...props }: PlaybackRateButtonProps = {}) {
  return (
    <PlaybackRateButtonPrimitive
      render={<Button />}
      className={(state) => `media-playback-rate-button ${resolveClassName(className, state)}`}
      {...props}
    />
  );
}

type RadioItemProps = Menu.RadioItemProps;

function RadioItem({ children, className, ...props }: RadioItemProps) {
  return (
    <Menu.RadioItem className={(state) => `media-menu-radio-item ${resolveClassName(className, state)}`} {...props}>
      {children}
      <Menu.ItemIndicator forceMount className={'media-menu-item-indicator'}>
        <CheckIconPrimitive className={'media-menu-radio-item-icon'} />
      </Menu.ItemIndicator>
    </Menu.RadioItem>
  );
}

function AudioSettingsMenu({ className, ...props }: AudioSettingsMenuProps = {}) {
  return (
    <MenuPrimitive.Root side="top" align="center" boundary="viewport" {...props}>
      <PlaybackRateRadioGroup.Root>
        <ButtonTooltip boundary="viewport" side="top">
          <MenuPrimitive.Trigger render={<PlaybackRateButton />} className={className} />
        </ButtonTooltip>
        <MenuPrimitive.Popup
          className={'media-popup media-popup-surface media-menu-popup audio-settings-menu-popup'}
        >
          <MenuPrimitive.Content className={'media-menu-content'}>
            <PlaybackRateRadioGroup.Options
              className={'media-menu-radio-group'}
              renderItem={(props, item) => (
                <RadioItem {...props}>
                  <span>{item.label}</span>
                </RadioItem>
              )}
            ></PlaybackRateRadioGroup.Options>
          </MenuPrimitive.Content>
        </MenuPrimitive.Popup>
      </PlaybackRateRadioGroup.Root>
    </MenuPrimitive.Root>
  );
}

interface VolumePopoverProps extends Omit<VolumePopoverPrimitive.RootProps, 'children'> {
  className?: ClassValue;
  orientation?: CoreVolumeSliderProps['orientation'];
  showTooltip?: boolean;
}

type MuteButtonProps = Omit<MuteButtonPrimitive.Props, 'children'>;

function MuteButton({ className, ...props }: MuteButtonProps = {}) {
  return (
    <MuteButtonPrimitive
      render={<Button />}
      className={(state) => `media-mute-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <VolumeOffIconPrimitive className={'media-button-icon media-mute-button-off-icon'} />
      <VolumeLowIconPrimitive className={'media-button-icon media-mute-button-low-icon'} />
      <VolumeHighIconPrimitive className={'media-button-icon media-mute-button-high-icon'} />
    </MuteButtonPrimitive>
  );
}

type VolumeSliderProps = Omit<VolumeSliderPrimitive.RootProps, 'children'>;

function VolumeSlider({ className, ...props }: VolumeSliderProps = {}) {
  return (
    <VolumeSliderPrimitive.Root
      className={(state) => `media-slider media-volume-slider ${resolveClassName(className, state)}`}
      thumbAlignment="edge"
      {...props}
    >
      <VolumeSliderPrimitive.Track render={<SliderTrack />}>
        <VolumeSliderPrimitive.Fill render={<SliderFill />} />
      </VolumeSliderPrimitive.Track>
      <VolumeSliderPrimitive.Thumb render={<SliderThumb />} className={'media-volume-slider-thumb'} />
    </VolumeSliderPrimitive.Root>
  );
}

function DefaultAudioControls() {
  return (
    <Controls.Root visibility="always">
      <Controls.Content className={'audio-controls audio-controls-content'}>
        <Tooltip.Provider>
          <Controls.Group className={'audio-controls-start'}>
            <AudioPlayButton />
            <ButtonTooltip boundary="viewport" side="top">
              <SeekButton className={'audio-seek-button'} seconds={-10} />
            </ButtonTooltip>
            <ButtonTooltip boundary="viewport" side="top">
              <SeekButton className={'audio-seek-button'} seconds={10} />
            </ButtonTooltip>
          </Controls.Group>

          <Controls.Group className={'audio-time-slider-group'}>
            <Time.Value className={'media-time-value'} type="current" />
            <AudioTimeSlider previewOverflow="visible" />
            <Time.Value className={'media-time-toggle audio-time-remaining-value'} type="remaining" toggle />
          </Controls.Group>

          <Controls.Group className={'audio-controls-end'}>
            <AudioSettingsMenu />
            <VolumePopover boundary="viewport" />
          </Controls.Group>
        </Tooltip.Provider>
      </Controls.Content>
    </Controls.Root>
  );
}

interface PlaybackHotkeysProps {
  disabled?: boolean | undefined;
}

function PlaybackHotkeys({ disabled = false }: PlaybackHotkeysProps = {}) {
  return (
    <>
      <Hotkey disabled={disabled} keys="Space" action="togglePaused" />
      <Hotkey disabled={disabled} keys="k" action="togglePaused" />
      <Hotkey disabled={disabled} keys="m" action="toggleMuted" />
      <Hotkey disabled={disabled} keys="ArrowRight" action="seekStep" />
      <Hotkey disabled={disabled} keys="ArrowLeft" action="seekStep" />
      <Hotkey disabled={disabled} keys="l" action="seekStep" />
      <Hotkey disabled={disabled} keys="j" action="seekStep" />
      <Hotkey disabled={disabled} keys="ArrowUp" action="volumeStep" />
      <Hotkey disabled={disabled} keys="ArrowDown" action="volumeStep" />
      <Hotkey disabled={disabled} keys="0-9" action="seekToPercent" />
      <Hotkey disabled={disabled} keys="Home" action="seekToPercent" value={0} />
      <Hotkey disabled={disabled} keys="End" action="seekToPercent" value={100} />
      <Hotkey disabled={disabled} keys=">" action="speedUp" />
      <Hotkey disabled={disabled} keys="<" action="speedDown" />
    </>
  );
}

type StatusAnnouncerProps = Omit<StatusAnnouncerPrimitive.Props, 'children'>;

function StatusAnnouncer({ className, ...props }: StatusAnnouncerProps = {}) {
  return (
    <StatusAnnouncerPrimitive
      className={(state) => `media-status-announcer ${resolveClassName(className, state)}`}
      {...props}
    />
  );
}

function DefaultAudioSkin({ children, className, ...props }: DefaultAudioSkinProps = {}) {
  return (
    <Container className={`audio-skin ${className ?? ''}`} data-theme="default" data-preset="audio" {...props}>
      {children}
      <AudioErrorDialog />
      <DefaultAudioControls />
      <PlaybackHotkeys />
      <StatusAnnouncer />
    </Container>
  );
}

const Skin = DefaultAudioSkin;

// ================================================================
// Player
// ================================================================

export interface AudioPlayerProps {
  children?: ReactNode;
  style?: CSSProperties;
  className?: string;
}

export function AudioSkin(props: AudioSkinProps) {
  return <Skin {...props} />;
}

// ================================================================
// Components
// ================================================================

function Button({ className, ...props }: ButtonProps) {
  return <button className={`media-button ${className ?? ''}`} {...props} />;
}

type SliderTrackProps = ComponentProps<'div'>;

function SliderTrack({ className, ...props }: SliderTrackProps) {
  return <div className={`media-slider-track ${className ?? ''}`} {...props} />;
}

type SliderBufferProps = ComponentProps<'div'>;

function SliderBuffer({ className, ...props }: SliderBufferProps) {
  return <div className={`media-slider-buffer ${className ?? ''}`} {...props} />;
}

type SliderFillProps = ComponentProps<'div'>;

function SliderFill({ className, ...props }: SliderFillProps) {
  return <div className={`media-slider-fill ${className ?? ''}`} {...props} />;
}

type SliderThumbProps = ComponentProps<'div'>;

function SliderThumb({ className, ...props }: SliderThumbProps) {
  return <div className={`media-slider-thumb ${className ?? ''}`} {...props} />;
}

function VolumePopover({
  className,
  showTooltip = false,
  side = 'top',
  orientation = 'vertical',
  ...props
}: VolumePopoverProps = {}) {
  return (
    <VolumePopoverPrimitive.Root openOnHover delay={200} closeDelay={100} side={side} {...props}>
      <ButtonTooltip delay={0} disabled={!showTooltip} sticky side="top">
        <VolumePopoverPrimitive.Trigger render={<MuteButton className={`${className ?? ''}`} />} />
      </ButtonTooltip>
      <VolumePopoverPrimitive.Popup
        className={'media-popup media-popup-safe-area media-popup-transition media-popup-surface media-volume-popover'}
      >
        <VolumeSlider orientation={orientation} />
      </VolumePopoverPrimitive.Popup>
    </VolumePopoverPrimitive.Root>
  );
}

Minimal video skin

'use client';

import { type ComponentProps, type SVGProps, type ReactElement, type ReactNode, type CSSProperties } from 'react';
import { ErrorDialog as ErrorDialogPrimitive, Container as ContainerPrimitive, Poster as PosterPrimitive, Gesture, Hotkey, SeekIndicator as SeekIndicatorPrimitive, StatusIndicator as StatusIndicatorPrimitive, VolumeIndicator as VolumeIndicatorPrimitive, StatusAnnouncer as StatusAnnouncerPrimitive, BufferingIndicator as BufferingIndicatorPrimitive, Tooltip, MuteButton as MuteButtonPrimitive, VolumeSlider as VolumeSliderPrimitive, VolumePopover as VolumePopoverPrimitive, Menu, Text as TextPrimitive, AudioTrackRadioGroup, CaptionsRadioGroup, PlaybackRateRadioGroup, QualityRadioGroup, AirPlayButton as AirPlayButtonPrimitive, CaptionsButton as CaptionsButtonPrimitive, CastButton as CastButtonPrimitive, FullscreenButton as FullscreenButtonPrimitive, PiPButton as PiPButtonPrimitive, PlayButton as PlayButtonPrimitive, TimeSlider as TimeSliderPrimitive, Slider, Controls, Time } from '@videojs/react';
import { Video } from '@videojs/react/video';
import './player.css';
import { Player } from './player';
import { ChevronIcon as ChevronIconPrimitive, CaptionsOnIcon as CaptionsOnIconPrimitive, CaptionsOffIcon as CaptionsOffIconPrimitive, FullscreenEnterIcon as FullscreenEnterIconPrimitive, FullscreenExitIcon as FullscreenExitIconPrimitive, PipEnterIcon as PipEnterIconPrimitive, PipExitIcon as PipExitIconPrimitive, PlayIcon as PlayIconPrimitive, PauseIcon as PauseIconPrimitive, VolumeHighIcon as VolumeHighIconPrimitive, VolumeLowIcon as VolumeLowIconPrimitive, VolumeOffIcon as VolumeOffIconPrimitive, CheckIcon as CheckIconPrimitive, SpeechIcon as SpeechIconPrimitive, SpeedIcon as SpeedIconPrimitive, SwitchesIcon as SwitchesIconPrimitive, GearIcon as GearIconPrimitive } from '@videojs/react/icons';
import { SpinnerIcon as SpinnerIconPrimitive, AirPlayEnterIcon as AirPlayEnterIconPrimitive, AirPlayExitIcon as AirPlayExitIconPrimitive, CaptionsOffIcon as CaptionsOffIconPrimitive, CaptionsOnIcon as CaptionsOnIconPrimitive, CastEnterIcon as CastEnterIconPrimitive, CastExitIcon as CastExitIconPrimitive, FullscreenEnterIcon as FullscreenEnterIconPrimitive, FullscreenExitIcon as FullscreenExitIconPrimitive, PipEnterIcon as PipEnterIconPrimitive, PipExitIcon as PipExitIconPrimitive, RestartIcon as RestartIconPrimitive, PlayIcon as PlayIconPrimitive, PauseIcon as PauseIconPrimitive } from '@videojs/react/icons/minimal';
import { type VolumeSliderProps as CoreVolumeSliderProps, type SliderPreviewOverflow } from '@videojs/core';
import { audioText, captionsText, speedText, qualityText, settingsText } from '@videojs/core/i18n/text/menu';
import { type Poster } from '@videojs/react';

type ContainerProps = ContainerPrimitive.Props;

function Container({ children, className, ...props }: ContainerProps) {
  return (
    <ContainerPrimitive className={`media-skin media-container ${className ?? ''}`} {...props}>
      {children}
    </ContainerPrimitive>
  );
}

interface PosterProps extends Omit<PosterPrimitive.Props, 'children' | 'render'> {
  children?: PosterPrimitive.Props['render'];
}

function Poster({ children, className, src, ...props }: PosterProps = {}) {
  return (
    <PosterPrimitive
      render={children}
      className={(state) => `media-poster ${resolveClassName(className, state)}`}
      src={src}
      {...props}
    />
  );
}

interface MinimalVideoSkinProps extends Omit<NonNullable<ComponentProps<typeof Container>>, 'children'> {
  src: string;
  poster?: string | undefined;
  renderPoster?: NonNullable<ComponentProps<typeof Poster>>['children'];
}

type BufferingIndicatorProps = Omit<BufferingIndicatorPrimitive.Props, 'children'>;

function BufferingIndicator({ className, ...props }: BufferingIndicatorProps = {}) {
  return (
    <BufferingIndicatorPrimitive
      className={(state) => `media-buffering-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <SpinnerIconPrimitive className={'media-buffering-indicator-spinner-icon'} />
    </BufferingIndicatorPrimitive>
  );
}

type ButtonProps = ComponentProps<'button'>;

interface ButtonTooltipProps extends Tooltip.RootProps {
  children: ReactElement;
  label?: ReactNode;
}

function ButtonTooltip({ children, label, ...props }: ButtonTooltipProps) {
  return (
    <Tooltip.Root {...props}>
      <Tooltip.Trigger render={children} />
      <Tooltip.Popup
        className={'media-popup media-popup-safe-area media-popup-transition media-popup-surface media-tooltip'}
      >
        {label ?? <Tooltip.Label />}
        {!label && <Tooltip.Shortcut className={'media-tooltip-shortcut'} />}
      </Tooltip.Popup>
    </Tooltip.Root>
  );
}

type PlayButtonProps = Omit<PlayButtonPrimitive.Props, 'children'>;

function PlayButton({ className, ...props }: PlayButtonProps = {}) {
  return (
    <PlayButtonPrimitive
      render={<Button />}
      className={(state) => `media-play-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <RestartIconPrimitive className={'media-button-icon media-play-button-restart-icon'} />
      <PlayIconPrimitive className={'media-button-icon media-play-button-play-icon'} />
      <PauseIconPrimitive className={'media-button-icon media-play-button-pause-icon'} />
    </PlayButtonPrimitive>
  );
}

interface VolumePopoverProps extends Omit<VolumePopoverPrimitive.RootProps, 'children'> {
  className?: ClassValue;
  orientation?: CoreVolumeSliderProps['orientation'];
  showTooltip?: boolean;
}

type MuteButtonProps = Omit<MuteButtonPrimitive.Props, 'children'>;

function MuteButton({ className, ...props }: MuteButtonProps = {}) {
  return (
    <MuteButtonPrimitive
      render={<Button />}
      className={(state) => `media-mute-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <VolumeOffIconPrimitive className={'media-button-icon media-mute-button-off-icon'} />
      <VolumeLowIconPrimitive className={'media-button-icon media-mute-button-low-icon'} />
      <VolumeHighIconPrimitive className={'media-button-icon media-mute-button-high-icon'} />
    </MuteButtonPrimitive>
  );
}

type VolumeSliderProps = Omit<VolumeSliderPrimitive.RootProps, 'children'>;

function VolumeSlider({ className, ...props }: VolumeSliderProps = {}) {
  return (
    <VolumeSliderPrimitive.Root
      className={(state) => `media-slider media-volume-slider ${resolveClassName(className, state)}`}
      thumbAlignment="edge"
      {...props}
    >
      <VolumeSliderPrimitive.Track render={<SliderTrack />}>
        <VolumeSliderPrimitive.Fill render={<SliderFill />} />
      </VolumeSliderPrimitive.Track>
      <VolumeSliderPrimitive.Thumb render={<SliderThumb />} className={'media-volume-slider-thumb'} />
    </VolumeSliderPrimitive.Root>
  );
}

interface TimeSliderProps extends Omit<TimeSliderPrimitive.RootProps, 'children'> {
  previewOverflow?: SliderPreviewOverflow | undefined;
}

function TimeSlider({ className, previewOverflow = 'visible', ...props }: TimeSliderProps = {}) {
  return (
    <TimeSliderPrimitive.Root
      className={(state) => `media-slider media-time-slider ${resolveClassName(className, state)}`}
      {...props}
    >
      <TimeSliderPrimitive.Chapters
        className={'media-time-slider-chapters'}
        renderChapter={(props) => (
          <div className={'media-time-slider-chapter'} {...props}>
            <TimeSliderPrimitive.Track render={<SliderTrack />} className={'media-time-slider-chapter-track'}>
              <TimeSliderPrimitive.Buffer render={<SliderBuffer />} />
              <TimeSliderPrimitive.Fill render={<SliderFill />} />
            </TimeSliderPrimitive.Track>
          </div>
        )}
      ></TimeSliderPrimitive.Chapters>
      <TimeSliderPrimitive.Thumb render={<SliderThumb />} className={'media-time-slider-thumb'} />
      <TimeSliderPrimitive.Preview className={'media-slider-preview'} overflow={previewOverflow}>
        <div className={'media-slider-preview-content media-popup-surface media-slider-thumbnail'}>
          <Slider.Thumbnail className={'media-slider-thumbnail-image'} />
          <SpinnerIconPrimitive className={'media-slider-thumbnail-spinner-icon'} />
        </div>
        <div className={'media-slider-preview-content media-time-slider-preview-content'}>
          <TimeSliderPrimitive.ChapterTitle className={'media-time-slider-chapter-title'} />
          <TimeSliderPrimitive.Value className={'media-time-slider-value'} type="pointer" />
        </div>
      </TimeSliderPrimitive.Preview>
    </TimeSliderPrimitive.Root>
  );
}

type CaptionsButtonProps = Omit<CaptionsButtonPrimitive.Props, 'children'>;

function CaptionsButton({ className, ...props }: CaptionsButtonProps = {}) {
  return (
    <CaptionsButtonPrimitive
      render={<Button />}
      className={(state) => `media-captions-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <CaptionsOffIconPrimitive className={'media-button-icon media-captions-button-off-icon'} />
      <CaptionsOnIconPrimitive className={'media-button-icon media-captions-button-on-icon'} />
    </CaptionsButtonPrimitive>
  );
}

interface SettingsMenuProps extends Menu.RootProps {
  className?: ClassValue;
}

function SettingsMenu({ children, className, ...props }: SettingsMenuProps) {
  return (
    <Menu.Root side="top" align="center" {...props}>
      <ButtonTooltip label={<TextPrimitive token={settingsText.key}>{settingsText.text}</TextPrimitive>} side="top">
        <Menu.Trigger
          render={<Button />}
          className={(state) => `media-settings-menu-trigger ${resolveClassName(className, state)}`}
        >
          <GearIconPrimitive className={'media-button-icon media-settings-menu-trigger-icon'} />
          <TextPrimitive className={'media-settings-menu-trigger-label'} token={settingsText.key}>
            {settingsText.text}
          </TextPrimitive>
        </Menu.Trigger>
      </ButtonTooltip>
      <Menu.Popup
        keepMounted
        className={'media-popup media-popup-surface media-menu-popup media-menu-resizable-popup'}
      >
        <Menu.Content className={'media-menu-content'}>{children}</Menu.Content>
      </Menu.Popup>
    </Menu.Root>
  );
}

type VideoSettingsMenuProps = Omit<NonNullable<ComponentProps<typeof SettingsMenu>>, 'children'>;

type QualityMenuProps = Omit<Menu.RootProps, 'children'>;

interface MenuChevronProps {
  back?: boolean;
  className?: ClassValue;
}

function MenuChevron({ back = false, className }: MenuChevronProps = {}) {
  return (
    <ChevronIconPrimitive className={`${back ? 'media-menu-back-chevron' : 'media-menu-forward-chevron'} ${className ?? ''}`} />
  );
}

type RadioItemProps = Menu.RadioItemProps;

function RadioItem({ children, className, ...props }: RadioItemProps) {
  return (
    <Menu.RadioItem className={(state) => `media-menu-radio-item ${resolveClassName(className, state)}`} {...props}>
      {children}
      <Menu.ItemIndicator forceMount className={'media-menu-item-indicator'}>
        <CheckIconPrimitive className={'media-menu-radio-item-icon'} />
      </Menu.ItemIndicator>
    </Menu.RadioItem>
  );
}

function QualityMenu({ ...props }: QualityMenuProps = {}) {
  return (
    <Menu.Root {...props}>
      <QualityRadioGroup.Root>
        <Menu.Trigger className={'media-menu-trigger-item'}>
          <SwitchesIconPrimitive className={'media-menu-trigger-item-icon'} />
          <TextPrimitive token={qualityText.key}>{qualityText.text}</TextPrimitive>
          <span className={'media-menu-hint'}>
            <QualityRadioGroup.Value className={'media-menu-hint-label'} />
            <MenuChevron />
          </span>
        </Menu.Trigger>
        <Menu.Content className={'media-menu-content'}>
          <Menu.Item className={'media-menu-back-item'}>
            <MenuChevron back />
            <TextPrimitive token={qualityText.key}>{qualityText.text}</TextPrimitive>
          </Menu.Item>
          <Menu.Separator className={'media-menu-separator'} />
          <QualityRadioGroup.Options
            className={'media-menu-radio-group'}
            renderItem={(props, item) => (
              <RadioItem {...props}>
                <span>
                  <span>{item.label}</span>
                  {item.tier ? <sup className={'media-menu-tier'}>{item.tier}</sup> : null}
                </span>
                {item.badge ? <span className={'media-menu-badge'}>{item.badge}</span> : null}
              </RadioItem>
            )}
          ></QualityRadioGroup.Options>
        </Menu.Content>
      </QualityRadioGroup.Root>
    </Menu.Root>
  );
}

type AudioTrackMenuProps = Omit<Menu.RootProps, 'children'>;

function AudioTrackMenu({ ...props }: AudioTrackMenuProps = {}) {
  return (
    <Menu.Root {...props}>
      <AudioTrackRadioGroup.Root>
        <Menu.Trigger className={'media-menu-trigger-item'}>
          <SpeechIconPrimitive className={'media-menu-trigger-item-icon'} />
          <TextPrimitive token={audioText.key}>{audioText.text}</TextPrimitive>
          <span className={'media-menu-hint'}>
            <AudioTrackRadioGroup.Value className={'media-menu-hint-label'} />
            <MenuChevron />
          </span>
        </Menu.Trigger>
        <Menu.Content className={'media-menu-content'}>
          <Menu.Item className={'media-menu-back-item'}>
            <MenuChevron back />
            <TextPrimitive token={audioText.key}>{audioText.text}</TextPrimitive>
          </Menu.Item>
          <Menu.Separator className={'media-menu-separator'} />
          <AudioTrackRadioGroup.Options
            className={'media-menu-radio-group'}
            renderItem={(props, item) => (
              <RadioItem {...props}>
                <span>{item.label}</span>
              </RadioItem>
            )}
          ></AudioTrackRadioGroup.Options>
        </Menu.Content>
      </AudioTrackRadioGroup.Root>
    </Menu.Root>
  );
}

type PlaybackRateSubmenuProps = Omit<Menu.RootProps, 'children'>;

function PlaybackRateSubmenu({ ...props }: PlaybackRateSubmenuProps = {}) {
  return (
    <Menu.Root {...props}>
      <PlaybackRateRadioGroup.Root>
        <Menu.Trigger className={'media-menu-trigger-item'}>
          <SpeedIconPrimitive className={'media-menu-trigger-item-icon'} />
          <TextPrimitive token={speedText.key}>{speedText.text}</TextPrimitive>
          <span className={'media-menu-hint'}>
            <PlaybackRateRadioGroup.Value className={'media-menu-hint-label'} />
            <MenuChevron />
          </span>
        </Menu.Trigger>
        <Menu.Content className={'media-menu-content'}>
          <Menu.Item className={'media-menu-back-item'}>
            <MenuChevron back />
            <TextPrimitive token={speedText.key}>{speedText.text}</TextPrimitive>
          </Menu.Item>
          <Menu.Separator className={'media-menu-separator'} />
          <PlaybackRateRadioGroup.Options
            className={'media-menu-radio-group'}
            renderItem={(props, item) => (
              <RadioItem {...props}>
                <span>{item.label}</span>
              </RadioItem>
            )}
          ></PlaybackRateRadioGroup.Options>
        </Menu.Content>
      </PlaybackRateRadioGroup.Root>
    </Menu.Root>
  );
}

type CaptionsSubmenuProps = Omit<Menu.RootProps, 'children'>;

function CaptionsSubmenu({ ...props }: CaptionsSubmenuProps = {}) {
  return (
    <Menu.Root {...props}>
      <CaptionsRadioGroup.Root>
        <Menu.Trigger className={'media-menu-trigger-item'}>
          <CaptionsOffIconPrimitive className={'media-menu-trigger-item-icon'} />
          <TextPrimitive token={captionsText.key}>{captionsText.text}</TextPrimitive>
          <span className={'media-menu-hint'}>
            <CaptionsRadioGroup.Value className={'media-menu-hint-label'} />
            <MenuChevron />
          </span>
        </Menu.Trigger>
        <Menu.Content className={'media-menu-content'}>
          <Menu.Item className={'media-menu-back-item'}>
            <MenuChevron back />
            <TextPrimitive token={captionsText.key}>{captionsText.text}</TextPrimitive>
          </Menu.Item>
          <Menu.Separator className={'media-menu-separator'} />
          <CaptionsRadioGroup.Options
            className={'media-menu-radio-group'}
            renderItem={(props, item) => (
              <RadioItem {...props}>
                <span>{item.label}</span>
              </RadioItem>
            )}
          ></CaptionsRadioGroup.Options>
        </Menu.Content>
      </CaptionsRadioGroup.Root>
    </Menu.Root>
  );
}

function VideoSettingsMenu(props: VideoSettingsMenuProps = {}) {
  return (
    <SettingsMenu {...props}>
      <QualityMenu />
      <AudioTrackMenu />
      <PlaybackRateSubmenu />
      <CaptionsSubmenu />
    </SettingsMenu>
  );
}

type CastButtonProps = Omit<CastButtonPrimitive.Props, 'children'>;

function CastButton({ className, ...props }: CastButtonProps = {}) {
  return (
    <CastButtonPrimitive
      render={<Button />}
      className={(state) => `media-cast-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <CastEnterIconPrimitive className={'media-button-icon media-cast-button-enter-icon'} />
      <CastExitIconPrimitive className={'media-button-icon media-cast-button-exit-icon'} />
    </CastButtonPrimitive>
  );
}

type AirPlayButtonProps = Omit<AirPlayButtonPrimitive.Props, 'children'>;

function AirPlayButton({ className, ...props }: AirPlayButtonProps = {}) {
  return (
    <AirPlayButtonPrimitive
      render={<Button />}
      className={(state) => `media-airplay-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <AirPlayEnterIconPrimitive className={'media-button-icon media-airplay-button-enter-icon'} />
      <AirPlayExitIconPrimitive className={'media-button-icon media-airplay-button-exit-icon'} />
    </AirPlayButtonPrimitive>
  );
}

type PiPButtonProps = Omit<PiPButtonPrimitive.Props, 'children'>;

function PiPButton({ className, ...props }: PiPButtonProps = {}) {
  return (
    <PiPButtonPrimitive
      render={<Button />}
      className={(state) => `media-pip-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <PipEnterIconPrimitive className={'media-button-icon media-pip-button-enter-icon'} />
      <PipExitIconPrimitive className={'media-button-icon media-pip-button-exit-icon'} />
    </PiPButtonPrimitive>
  );
}

type FullscreenButtonProps = Omit<FullscreenButtonPrimitive.Props, 'children'>;

function FullscreenButton({ className, ...props }: FullscreenButtonProps = {}) {
  return (
    <FullscreenButtonPrimitive
      render={<Button />}
      className={(state) => `media-fullscreen-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <FullscreenEnterIconPrimitive className={'media-button-icon media-fullscreen-button-enter-icon'} />
      <FullscreenExitIconPrimitive className={'media-button-icon media-fullscreen-button-exit-icon'} />
    </FullscreenButtonPrimitive>
  );
}

function MinimalVideoControls() {
  return (
    <Controls.Root>
      <Controls.Backdrop className={'video-controls-backdrop'} />
      <Controls.Content className={'video-controls video-controls-content video-controls-wrap'}>
        <Tooltip.Provider>
          <Controls.Group className={'video-controls-start'}>
            <ButtonTooltip side="top">
              <PlayButton />
            </ButtonTooltip>
            <VolumePopover showTooltip side="right" orientation="horizontal" />
          </Controls.Group>

          <Controls.Group className={'video-time-slider-group'}>
            <Time.Group className={'media-time-group'}>
              <Time.Value className={'media-time-toggle media-time-current-value'} type="current" toggle />
              <Time.Separator className={'media-time-separator'} />
              <Time.Value className={'media-time-duration-value'} type="duration" />
            </Time.Group>
            <TimeSlider previewOverflow="clamp" />
          </Controls.Group>

          <Controls.Group className={'video-controls-end'}>
            <ButtonTooltip side="top">
              <CaptionsButton />
            </ButtonTooltip>
            <VideoSettingsMenu />
            <Controls.Group className={'video-controls-trailing'}>
              <ButtonTooltip side="top">
                <CastButton />
              </ButtonTooltip>
              <ButtonTooltip side="top">
                <AirPlayButton />
              </ButtonTooltip>
              <ButtonTooltip side="top">
                <PiPButton />
              </ButtonTooltip>
              <ButtonTooltip side="top">
                <FullscreenButton />
              </ButtonTooltip>
            </Controls.Group>
          </Controls.Group>
        </Tooltip.Provider>
      </Controls.Content>
    </Controls.Root>
  );
}

interface VideoHotkeysProps {
  disabled?: boolean | undefined;
}

interface PlaybackHotkeysProps {
  disabled?: boolean | undefined;
}

function PlaybackHotkeys({ disabled = false }: PlaybackHotkeysProps = {}) {
  return (
    <>
      <Hotkey disabled={disabled} keys="Space" action="togglePaused" />
      <Hotkey disabled={disabled} keys="k" action="togglePaused" />
      <Hotkey disabled={disabled} keys="m" action="toggleMuted" />
      <Hotkey disabled={disabled} keys="ArrowRight" action="seekStep" />
      <Hotkey disabled={disabled} keys="ArrowLeft" action="seekStep" />
      <Hotkey disabled={disabled} keys="l" action="seekStep" />
      <Hotkey disabled={disabled} keys="j" action="seekStep" />
      <Hotkey disabled={disabled} keys="ArrowUp" action="volumeStep" />
      <Hotkey disabled={disabled} keys="ArrowDown" action="volumeStep" />
      <Hotkey disabled={disabled} keys="0-9" action="seekToPercent" />
      <Hotkey disabled={disabled} keys="Home" action="seekToPercent" value={0} />
      <Hotkey disabled={disabled} keys="End" action="seekToPercent" value={100} />
      <Hotkey disabled={disabled} keys=">" action="speedUp" />
      <Hotkey disabled={disabled} keys="<" action="speedDown" />
    </>
  );
}

function VideoHotkeys({ disabled = false }: VideoHotkeysProps = {}) {
  return (
    <>
      <PlaybackHotkeys disabled={disabled} />
      <Hotkey disabled={disabled} keys="f" action="toggleFullscreen" />
      <Hotkey disabled={disabled} keys="c" action="toggleSubtitles" />
      <Hotkey disabled={disabled} keys="i" action="togglePictureInPicture" />
    </>
  );
}

interface VideoGesturesProps {
  disabled?: boolean | undefined;
}

function VideoGestures({ disabled = false }: VideoGesturesProps = {}) {
  return (
    <>
      <Gesture disabled={disabled} type="tap" action="togglePaused" pointer="mouse" region="center" />
      <Gesture disabled={disabled} type="tap" action="toggleControls" pointer="touch" />
      <Gesture disabled={disabled} type="doubletap" action="seekStep" region="left" />
      <Gesture disabled={disabled} type="doubletap" action="toggleFullscreen" region="center" />
      <Gesture disabled={disabled} type="doubletap" action="seekStep" region="right" />
    </>
  );
}

type VideoStatusIndicatorsProps = Omit<ComponentProps<'div'>, 'children'>;

type StatusAnnouncerProps = Omit<StatusAnnouncerPrimitive.Props, 'children'>;

function StatusAnnouncer({ className, ...props }: StatusAnnouncerProps = {}) {
  return (
    <StatusAnnouncerPrimitive
      className={(state) => `media-status-announcer ${resolveClassName(className, state)}`}
      {...props}
    />
  );
}

type VolumeIndicatorProps = Omit<VolumeIndicatorPrimitive.RootProps, 'children'>;

function VolumeIndicator({ className, ...props }: VolumeIndicatorProps = {}) {
  return (
    <VolumeIndicatorPrimitive.Root
      className={(state) => `media-indicator media-volume-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <VolumeIndicatorPrimitive.Fill className={'media-indicator-content media-volume-indicator-fill'}>
        <VolumeHighIconPrimitive className={'media-volume-indicator-high-icon'} />
        <VolumeLowIconPrimitive className={'media-volume-indicator-low-icon'} />
        <VolumeOffIconPrimitive className={'media-volume-indicator-off-icon'} />
        <VolumeIndicatorPrimitive.Value className={'media-volume-indicator-value'} />
      </VolumeIndicatorPrimitive.Fill>
    </VolumeIndicatorPrimitive.Root>
  );
}

type StatusIndicatorProps = Omit<StatusIndicatorPrimitive.RootProps, 'children'>;

const TOP_STATUS_ACTIONS = ['toggleSubtitles', 'toggleFullscreen', 'togglePictureInPicture'] as const;

function StatusIndicator({ className, ...props }: StatusIndicatorProps = {}) {
  return (
    <StatusIndicatorPrimitive.Root
      actions={TOP_STATUS_ACTIONS}
      className={(state) => `media-indicator media-status-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <div className={'media-indicator-content media-status-indicator-content'}>
        <CaptionsOnIconPrimitive className={'media-status-indicator-captions-on-icon'} />
        <CaptionsOffIconPrimitive className={'media-status-indicator-captions-off-icon'} />
        <FullscreenEnterIconPrimitive className={'media-status-indicator-fullscreen-enter-icon'} />
        <FullscreenExitIconPrimitive className={'media-status-indicator-fullscreen-exit-icon'} />
        <PipEnterIconPrimitive className={'media-status-indicator-pip-enter-icon'} />
        <PipExitIconPrimitive className={'media-status-indicator-pip-exit-icon'} />
        <StatusIndicatorPrimitive.Value className={'media-status-indicator-value'} />
      </div>
    </StatusIndicatorPrimitive.Root>
  );
}

type SeekIndicatorProps = Omit<SeekIndicatorPrimitive.RootProps, 'children'>;

function SeekIndicator({ className, ...props }: SeekIndicatorProps = {}) {
  return (
    <SeekIndicatorPrimitive.Root
      className={(state) => `media-seek-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <ChevronIconPrimitive className={'media-seek-indicator-icon'} />
      <SeekIndicatorPrimitive.Value className={'media-seek-indicator-value'} />
    </SeekIndicatorPrimitive.Root>
  );
}

type PlaybackStatusIndicatorProps = Omit<StatusIndicatorPrimitive.RootProps, 'children'>;

const PLAYBACK_STATUS_ACTIONS = ['togglePaused'] as const;

function PlaybackStatusIndicator({ className, ...props }: PlaybackStatusIndicatorProps = {}) {
  return (
    <StatusIndicatorPrimitive.Root
      actions={PLAYBACK_STATUS_ACTIONS}
      className={(state) => `media-playback-status-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <PlayIconPrimitive className={'media-playback-status-indicator-play-icon'} />
      <PauseIconPrimitive className={'media-playback-status-indicator-pause-icon'} />
    </StatusIndicatorPrimitive.Root>
  );
}

function VideoStatusIndicators({ className, ...props }: VideoStatusIndicatorsProps = {}) {
  return (
    <>
      <StatusAnnouncer />
      <div className={`video-status-indicators ${className ?? ''}`} {...props}>
        <VolumeIndicator />
        <StatusIndicator />
        <SeekIndicator />
        <PlaybackStatusIndicator />
      </div>
    </>
  );
}

function MinimalVideoSkin({ children, className, renderPoster, ...props }: MinimalVideoSkinProps = {}) {
  return (
    <Container className={`video-skin ${className ?? ''}`} data-theme="minimal" data-preset="video" {...props}>
      {children}
      <Poster>{renderPoster}</Poster>
      <BufferingIndicator />
      <ErrorDialog />

      <MinimalVideoControls />

      <VideoHotkeys />
      <VideoGestures />
      <VideoStatusIndicators />
    </Container>
  );
}

const Skin = MinimalVideoSkin;

// ================================================================
// Player
// ================================================================

export interface VideoPlayerProps {
  children?: ReactNode;
  style?: CSSProperties;
  className?: string;
  renderPoster?: Poster.Props['render'];
}

export function MinimalVideoSkin(props: MinimalVideoSkinProps) {
  return <Skin {...props} />;
}

// ================================================================
// Components
// ================================================================

function Button({ className, ...props }: ButtonProps) {
  return <button className={`media-button ${className ?? ''}`} {...props} />;
}

type SliderTrackProps = ComponentProps<'div'>;

function SliderTrack({ className, ...props }: SliderTrackProps) {
  return <div className={`media-slider-track ${className ?? ''}`} {...props} />;
}

type SliderFillProps = ComponentProps<'div'>;

function SliderFill({ className, ...props }: SliderFillProps) {
  return <div className={`media-slider-fill ${className ?? ''}`} {...props} />;
}

type SliderThumbProps = ComponentProps<'div'>;

function SliderThumb({ className, ...props }: SliderThumbProps) {
  return <div className={`media-slider-thumb ${className ?? ''}`} {...props} />;
}

function VolumePopover({
  className,
  showTooltip = false,
  side = 'top',
  orientation = 'vertical',
  ...props
}: VolumePopoverProps = {}) {
  return (
    <VolumePopoverPrimitive.Root openOnHover delay={200} closeDelay={100} side={side} {...props}>
      <ButtonTooltip delay={0} disabled={!showTooltip} sticky side="top">
        <VolumePopoverPrimitive.Trigger render={<MuteButton className={`${className ?? ''}`} />} />
      </ButtonTooltip>
      <VolumePopoverPrimitive.Popup
        className={'media-popup media-popup-safe-area media-popup-transition media-popup-surface media-volume-popover'}
      >
        <VolumeSlider orientation={orientation} />
      </VolumePopoverPrimitive.Popup>
    </VolumePopoverPrimitive.Root>
  );
}

type SliderBufferProps = ComponentProps<'div'>;

function SliderBuffer({ className, ...props }: SliderBufferProps) {
  return <div className={`media-slider-buffer ${className ?? ''}`} {...props} />;
}

// ================================================================
// Error Dialog
// ================================================================

function ErrorDialog() {
  return (
    <ErrorDialogPrimitive.Root>
      <ErrorDialogPrimitive.Backdrop className={'media-dialog-backdrop'} />
      <ErrorDialogPrimitive.Popup className={'media-dialog-popup'}>
        <div className={'media-dialog-content'}>
          <ErrorDialogPrimitive.Title className={'media-dialog-title'} />
          <ErrorDialogPrimitive.Description className={'media-dialog-description'} />
        </div>
        <div className={'media-dialog-actions'}>
          <ErrorDialogPrimitive.Close render={<Button />} className={'media-dialog-close'} />
        </div>
      </ErrorDialogPrimitive.Popup>
    </ErrorDialogPrimitive.Root>
  );
}

Minimal audio skin

'use client';

import { StatusAnnouncer as StatusAnnouncerPrimitive, Container as ContainerPrimitive, ErrorDialog, Hotkey, Tooltip, MuteButton as MuteButtonPrimitive, VolumeSlider as VolumeSliderPrimitive, VolumePopover as VolumePopoverPrimitive, PlayButton as PlayButtonPrimitive, BufferingIndicator as BufferingIndicatorPrimitive, Menu, PlaybackRateButton as PlaybackRateButtonPrimitive, PlaybackRateRadioGroup, TimeSlider, SeekButton as SeekButtonPrimitive, Controls, Time } from '@videojs/react';
import { Audio } from '@videojs/react/audio';
import './player.css';
import { Player } from './player';
import { type ComponentProps, type ReactElement, type ReactNode, type SVGProps, type CSSProperties } from 'react';
import { VolumeOffIcon as VolumeOffIconPrimitive, VolumeLowIcon as VolumeLowIconPrimitive, VolumeHighIcon as VolumeHighIconPrimitive, RestartIcon as RestartIconPrimitive, PlayIcon as PlayIconPrimitive, PauseIcon as PauseIconPrimitive, SpinnerIcon as SpinnerIconPrimitive, CheckIcon as CheckIconPrimitive } from '@videojs/react/icons';
import { type VolumeSliderProps as CoreVolumeSliderProps, type SliderPreviewOverflow } from '@videojs/core';
import { SeekIcon as SeekIconPrimitive } from '@videojs/react/icons/minimal';
import { type Poster } from '@videojs/react';

type ContainerProps = ContainerPrimitive.Props;

function Container({ children, className, ...props }: ContainerProps) {
  return (
    <ContainerPrimitive className={`media-skin media-container ${className ?? ''}`} {...props}>
      {children}
    </ContainerPrimitive>
  );
}

interface MinimalAudioSkinProps extends Omit<NonNullable<ComponentProps<typeof Container>>, 'children'> {
  src: string;
}

type ButtonProps = ComponentProps<'button'>;

function AudioErrorDialog() {
  return (
    <ErrorDialog.Root>
      <ErrorDialog.Backdrop className={'audio-dialog-backdrop'} />
      <ErrorDialog.Popup className={'audio-dialog-popup'}>
        <div className={'audio-dialog-content'}>
          <ErrorDialog.Title className={'audio-dialog-title'} />
          <ErrorDialog.Description className={'audio-dialog-description'} />
        </div>
        <div className={'audio-dialog-actions'}>
          <ErrorDialog.Close render={<Button />} className={'audio-dialog-close'} />
        </div>
      </ErrorDialog.Popup>
    </ErrorDialog.Root>
  );
}

type AudioPlayButtonProps = Omit<ComponentProps<'div'>, 'children'>;

type BufferingIndicatorProps = Omit<BufferingIndicatorPrimitive.Props, 'children'>;

function BufferingIndicator({ className, ...props }: BufferingIndicatorProps = {}) {
  return (
    <BufferingIndicatorPrimitive
      className={(state) => `media-buffering-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <SpinnerIconPrimitive className={'media-buffering-indicator-spinner-icon'} />
    </BufferingIndicatorPrimitive>
  );
}

interface ButtonTooltipProps extends Tooltip.RootProps {
  children: ReactElement;
  label?: ReactNode;
}

function ButtonTooltip({ children, label, ...props }: ButtonTooltipProps) {
  return (
    <Tooltip.Root {...props}>
      <Tooltip.Trigger render={children} />
      <Tooltip.Popup
        className={'media-popup media-popup-safe-area media-popup-transition media-popup-surface media-tooltip'}
      >
        {label ?? <Tooltip.Label />}
        {!label && <Tooltip.Shortcut className={'media-tooltip-shortcut'} />}
      </Tooltip.Popup>
    </Tooltip.Root>
  );
}

type PlayButtonProps = Omit<PlayButtonPrimitive.Props, 'children'>;

function PlayButton({ className, ...props }: PlayButtonProps = {}) {
  return (
    <PlayButtonPrimitive
      render={<Button />}
      className={(state) => `media-play-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <RestartIconPrimitive className={'media-button-icon media-play-button-restart-icon'} />
      <PlayIconPrimitive className={'media-button-icon media-play-button-play-icon'} />
      <PauseIconPrimitive className={'media-button-icon media-play-button-pause-icon'} />
    </PlayButtonPrimitive>
  );
}

function AudioPlayButton({ className, ...props }: AudioPlayButtonProps = {}) {
  return (
    <div className={`audio-play-button ${className ?? ''}`} {...props}>
      <BufferingIndicator className={'audio-play-button-buffering-indicator'} />
      <ButtonTooltip boundary="viewport" side="top">
        <PlayButton />
      </ButtonTooltip>
    </div>
  );
}

type SeekButtonProps = Omit<SeekButtonPrimitive.Props, 'children'>;

function SeekButton({ className, seconds = 10, ...props }: SeekButtonProps = {}) {
  return (
    <SeekButtonPrimitive
      render={<Button />}
      className={(state) => `media-seek-button ${resolveClassName(className, state)}`}
      seconds={seconds}
      {...props}
    >
      <div className={'media-seek-button-content'}>
        <SeekIconPrimitive className={`media-button-icon ${seconds < 0 && 'media-seek-button-backward-icon'}`} />
        <span
          className={`media-seek-button-label ${seconds < 0 ? 'media-seek-button-backward-label' : 'media-seek-button-forward-label'}`}
        >
          {Math.abs(seconds)}
        </span>
      </div>
    </SeekButtonPrimitive>
  );
}

interface AudioTimeSliderProps extends Omit<TimeSlider.RootProps, 'children'> {
  previewOverflow?: SliderPreviewOverflow | undefined;
}

function AudioTimeSlider({ className, previewOverflow = 'clamp', ...props }: AudioTimeSliderProps = {}) {
  return (
    <TimeSlider.Root
      className={(state) => `media-slider audio-time-slider ${resolveClassName(className, state)}`}
      {...props}
    >
      <TimeSlider.Track render={<SliderTrack />}>
        <TimeSlider.Buffer render={<SliderBuffer />} />
        <TimeSlider.Fill render={<SliderFill />} />
      </TimeSlider.Track>
      <TimeSlider.Thumb render={<SliderThumb />} className={'audio-time-slider-thumb'} />
      <TimeSlider.Preview className={'media-slider-preview'} overflow={previewOverflow}>
        <div
          className={'media-slider-preview-content media-popup-surface media-tooltip audio-time-slider-preview-content'}
        >
          <TimeSlider.Value className={'audio-time-slider-value'} type="pointer" />
        </div>
      </TimeSlider.Preview>
    </TimeSlider.Root>
  );
}

interface VolumePopoverProps extends Omit<VolumePopoverPrimitive.RootProps, 'children'> {
  className?: ClassValue;
  orientation?: CoreVolumeSliderProps['orientation'];
  showTooltip?: boolean;
}

type MuteButtonProps = Omit<MuteButtonPrimitive.Props, 'children'>;

function MuteButton({ className, ...props }: MuteButtonProps = {}) {
  return (
    <MuteButtonPrimitive
      render={<Button />}
      className={(state) => `media-mute-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <VolumeOffIconPrimitive className={'media-button-icon media-mute-button-off-icon'} />
      <VolumeLowIconPrimitive className={'media-button-icon media-mute-button-low-icon'} />
      <VolumeHighIconPrimitive className={'media-button-icon media-mute-button-high-icon'} />
    </MuteButtonPrimitive>
  );
}

type VolumeSliderProps = Omit<VolumeSliderPrimitive.RootProps, 'children'>;

function VolumeSlider({ className, ...props }: VolumeSliderProps = {}) {
  return (
    <VolumeSliderPrimitive.Root
      className={(state) => `media-slider media-volume-slider ${resolveClassName(className, state)}`}
      thumbAlignment="edge"
      {...props}
    >
      <VolumeSliderPrimitive.Track render={<SliderTrack />}>
        <VolumeSliderPrimitive.Fill render={<SliderFill />} />
      </VolumeSliderPrimitive.Track>
      <VolumeSliderPrimitive.Thumb render={<SliderThumb />} className={'media-volume-slider-thumb'} />
    </VolumeSliderPrimitive.Root>
  );
}

interface AudioSettingsMenuProps extends Omit<MenuPrimitive.RootProps, 'children'> {
  className?: MenuPrimitive.TriggerProps['className'];
}

type PlaybackRateButtonProps = Omit<PlaybackRateButtonPrimitive.Props, 'children'>;

function PlaybackRateButton({ className, ...props }: PlaybackRateButtonProps = {}) {
  return (
    <PlaybackRateButtonPrimitive
      render={<Button />}
      className={(state) => `media-playback-rate-button ${resolveClassName(className, state)}`}
      {...props}
    />
  );
}

type RadioItemProps = Menu.RadioItemProps;

function RadioItem({ children, className, ...props }: RadioItemProps) {
  return (
    <Menu.RadioItem className={(state) => `media-menu-radio-item ${resolveClassName(className, state)}`} {...props}>
      {children}
      <Menu.ItemIndicator forceMount className={'media-menu-item-indicator'}>
        <CheckIconPrimitive className={'media-menu-radio-item-icon'} />
      </Menu.ItemIndicator>
    </Menu.RadioItem>
  );
}

function AudioSettingsMenu({ className, ...props }: AudioSettingsMenuProps = {}) {
  return (
    <MenuPrimitive.Root side="top" align="center" boundary="viewport" {...props}>
      <PlaybackRateRadioGroup.Root>
        <ButtonTooltip boundary="viewport" side="top">
          <MenuPrimitive.Trigger render={<PlaybackRateButton />} className={className} />
        </ButtonTooltip>
        <MenuPrimitive.Popup
          className={'media-popup media-popup-surface media-menu-popup audio-settings-menu-popup'}
        >
          <MenuPrimitive.Content className={'media-menu-content'}>
            <PlaybackRateRadioGroup.Options
              className={'media-menu-radio-group'}
              renderItem={(props, item) => (
                <RadioItem {...props}>
                  <span>{item.label}</span>
                </RadioItem>
              )}
            ></PlaybackRateRadioGroup.Options>
          </MenuPrimitive.Content>
        </MenuPrimitive.Popup>
      </PlaybackRateRadioGroup.Root>
    </MenuPrimitive.Root>
  );
}

function MinimalAudioControls() {
  return (
    <Controls.Root visibility="always">
      <Controls.Content className={'audio-controls audio-controls-content'}>
        <Tooltip.Provider>
          <Controls.Group className={'audio-controls-start'}>
            <AudioPlayButton />
            <ButtonTooltip boundary="viewport" side="top">
              <SeekButton seconds={-10} />
            </ButtonTooltip>
            <ButtonTooltip boundary="viewport" side="top">
              <SeekButton seconds={10} />
            </ButtonTooltip>
          </Controls.Group>

          <Controls.Group className={'audio-time-slider-group'}>
            <Time.Group className={'media-time-group'}>
              <Time.Value className={'media-time-toggle media-time-current-value'} type="current" toggle />
              <Time.Separator className={'media-time-separator'} />
              <Time.Value className={'media-time-duration-value'} type="duration" />
            </Time.Group>
            <AudioTimeSlider />
          </Controls.Group>

          <Controls.Group className={'audio-controls-end'}>
            <VolumePopover boundary="viewport" showTooltip side="left" orientation="horizontal" />
            <AudioSettingsMenu />
          </Controls.Group>
        </Tooltip.Provider>
      </Controls.Content>
    </Controls.Root>
  );
}

interface PlaybackHotkeysProps {
  disabled?: boolean | undefined;
}

function PlaybackHotkeys({ disabled = false }: PlaybackHotkeysProps = {}) {
  return (
    <>
      <Hotkey disabled={disabled} keys="Space" action="togglePaused" />
      <Hotkey disabled={disabled} keys="k" action="togglePaused" />
      <Hotkey disabled={disabled} keys="m" action="toggleMuted" />
      <Hotkey disabled={disabled} keys="ArrowRight" action="seekStep" />
      <Hotkey disabled={disabled} keys="ArrowLeft" action="seekStep" />
      <Hotkey disabled={disabled} keys="l" action="seekStep" />
      <Hotkey disabled={disabled} keys="j" action="seekStep" />
      <Hotkey disabled={disabled} keys="ArrowUp" action="volumeStep" />
      <Hotkey disabled={disabled} keys="ArrowDown" action="volumeStep" />
      <Hotkey disabled={disabled} keys="0-9" action="seekToPercent" />
      <Hotkey disabled={disabled} keys="Home" action="seekToPercent" value={0} />
      <Hotkey disabled={disabled} keys="End" action="seekToPercent" value={100} />
      <Hotkey disabled={disabled} keys=">" action="speedUp" />
      <Hotkey disabled={disabled} keys="<" action="speedDown" />
    </>
  );
}

type StatusAnnouncerProps = Omit<StatusAnnouncerPrimitive.Props, 'children'>;

function StatusAnnouncer({ className, ...props }: StatusAnnouncerProps = {}) {
  return (
    <StatusAnnouncerPrimitive
      className={(state) => `media-status-announcer ${resolveClassName(className, state)}`}
      {...props}
    />
  );
}

function MinimalAudioSkin({ children, className, ...props }: MinimalAudioSkinProps = {}) {
  return (
    <Container className={`audio-skin ${className ?? ''}`} data-theme="minimal" data-preset="audio" {...props}>
      {children}
      <AudioErrorDialog />
      <MinimalAudioControls />
      <PlaybackHotkeys />
      <StatusAnnouncer />
    </Container>
  );
}

const Skin = MinimalAudioSkin;

// ================================================================
// Player
// ================================================================

export interface AudioPlayerProps {
  children?: ReactNode;
  style?: CSSProperties;
  className?: string;
}

export function MinimalAudioSkin(props: MinimalAudioSkinProps) {
  return <Skin {...props} />;
}

// ================================================================
// Components
// ================================================================

function Button({ className, ...props }: ButtonProps) {
  return <button className={`media-button ${className ?? ''}`} {...props} />;
}

type SliderTrackProps = ComponentProps<'div'>;

function SliderTrack({ className, ...props }: SliderTrackProps) {
  return <div className={`media-slider-track ${className ?? ''}`} {...props} />;
}

type SliderBufferProps = ComponentProps<'div'>;

function SliderBuffer({ className, ...props }: SliderBufferProps) {
  return <div className={`media-slider-buffer ${className ?? ''}`} {...props} />;
}

type SliderFillProps = ComponentProps<'div'>;

function SliderFill({ className, ...props }: SliderFillProps) {
  return <div className={`media-slider-fill ${className ?? ''}`} {...props} />;
}

type SliderThumbProps = ComponentProps<'div'>;

function SliderThumb({ className, ...props }: SliderThumbProps) {
  return <div className={`media-slider-thumb ${className ?? ''}`} {...props} />;
}

function VolumePopover({
  className,
  showTooltip = false,
  side = 'top',
  orientation = 'vertical',
  ...props
}: VolumePopoverProps = {}) {
  return (
    <VolumePopoverPrimitive.Root openOnHover delay={200} closeDelay={100} side={side} {...props}>
      <ButtonTooltip delay={0} disabled={!showTooltip} sticky side="top">
        <VolumePopoverPrimitive.Trigger render={<MuteButton className={`${className ?? ''}`} />} />
      </ButtonTooltip>
      <VolumePopoverPrimitive.Popup
        className={'media-popup media-popup-safe-area media-popup-transition media-popup-surface media-volume-popover'}
      >
        <VolumeSlider orientation={orientation} />
      </VolumePopoverPrimitive.Popup>
    </VolumePopoverPrimitive.Root>
  );
}

Default live video skin

The live skins swap the duration and current-time displays for a Live button. They build on the live presets, so the player reports live-edge state.

'use client';

import { type ComponentProps, type SVGProps, type ReactElement, type ReactNode, type CSSProperties } from 'react';
import { ErrorDialog as ErrorDialogPrimitive, Container as ContainerPrimitive, Poster as PosterPrimitive, Gesture, Hotkey, StatusIndicator as StatusIndicatorPrimitive, VolumeIndicator as VolumeIndicatorPrimitive, StatusAnnouncer as StatusAnnouncerPrimitive, BufferingIndicator as BufferingIndicatorPrimitive, Tooltip, LiveButton as LiveButtonPrimitive, MuteButton as MuteButtonPrimitive, VolumeSlider as VolumeSliderPrimitive, VolumePopover as VolumePopoverPrimitive, CaptionsButton as CaptionsButtonPrimitive, Menu, CaptionsRadioGroup, AirPlayButton as AirPlayButtonPrimitive, CastButton as CastButtonPrimitive, FullscreenButton as FullscreenButtonPrimitive, PiPButton as PiPButtonPrimitive, PlayButton as PlayButtonPrimitive, Controls } from '@videojs/react';
import { HlsJsVideo } from '@videojs/react/media/hlsjs-video';
import './player.css';
import { Player } from './player';
import { CaptionsOnIcon as CaptionsOnIconPrimitive, CaptionsOffIcon as CaptionsOffIconPrimitive, FullscreenEnterIcon as FullscreenEnterIconPrimitive, FullscreenExitIcon as FullscreenExitIconPrimitive, PipEnterIcon as PipEnterIconPrimitive, PipExitIcon as PipExitIconPrimitive, PlayIcon as PlayIconPrimitive, PauseIcon as PauseIconPrimitive, VolumeHighIcon as VolumeHighIconPrimitive, VolumeLowIcon as VolumeLowIconPrimitive, VolumeOffIcon as VolumeOffIconPrimitive, SpinnerIcon as SpinnerIconPrimitive, CheckIcon as CheckIconPrimitive, AirPlayEnterIcon as AirPlayEnterIconPrimitive, AirPlayExitIcon as AirPlayExitIconPrimitive, CastEnterIcon as CastEnterIconPrimitive, CastExitIcon as CastExitIconPrimitive, RestartIcon as RestartIconPrimitive } from '@videojs/react/icons';
import { type VolumeSliderProps as CoreVolumeSliderProps } from '@videojs/core';
import { type Poster } from '@videojs/react';

type ContainerProps = ContainerPrimitive.Props;

function Container({ children, className, ...props }: ContainerProps) {
  return (
    <ContainerPrimitive className={`media-skin media-container ${className ?? ''}`} {...props}>
      {children}
    </ContainerPrimitive>
  );
}

interface PosterProps extends Omit<PosterPrimitive.Props, 'children' | 'render'> {
  children?: PosterPrimitive.Props['render'];
}

function Poster({ children, className, src, ...props }: PosterProps = {}) {
  return (
    <PosterPrimitive
      render={children}
      className={(state) => `media-poster ${resolveClassName(className, state)}`}
      src={src}
      {...props}
    />
  );
}

interface DefaultLiveVideoSkinProps extends Omit<NonNullable<ComponentProps<typeof Container>>, 'children'> {
  src: string;
  poster?: string | undefined;
  renderPoster?: NonNullable<ComponentProps<typeof Poster>>['children'];
}

type BufferingIndicatorProps = Omit<BufferingIndicatorPrimitive.Props, 'children'>;

function BufferingIndicator({ className, ...props }: BufferingIndicatorProps = {}) {
  return (
    <BufferingIndicatorPrimitive
      className={(state) => `media-buffering-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <SpinnerIconPrimitive className={'media-buffering-indicator-spinner-icon'} />
    </BufferingIndicatorPrimitive>
  );
}

type ButtonProps = ComponentProps<'button'>;

interface ButtonTooltipProps extends Tooltip.RootProps {
  children: ReactElement;
  label?: ReactNode;
}

function ButtonTooltip({ children, label, ...props }: ButtonTooltipProps) {
  return (
    <Tooltip.Root {...props}>
      <Tooltip.Trigger render={children} />
      <Tooltip.Popup
        className={'media-popup media-popup-safe-area media-popup-transition media-popup-surface media-tooltip'}
      >
        {label ?? <Tooltip.Label />}
        {!label && <Tooltip.Shortcut className={'media-tooltip-shortcut'} />}
      </Tooltip.Popup>
    </Tooltip.Root>
  );
}

type PlayButtonProps = Omit<PlayButtonPrimitive.Props, 'children'>;

function PlayButton({ className, ...props }: PlayButtonProps = {}) {
  return (
    <PlayButtonPrimitive
      render={<Button />}
      className={(state) => `media-play-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <RestartIconPrimitive className={'media-button-icon media-play-button-restart-icon'} />
      <PlayIconPrimitive className={'media-button-icon media-play-button-play-icon'} />
      <PauseIconPrimitive className={'media-button-icon media-play-button-pause-icon'} />
    </PlayButtonPrimitive>
  );
}

type LiveButtonProps = Omit<LiveButtonPrimitive.Props, 'children'>;

function LiveButton({ className, ...props }: LiveButtonProps = {}) {
  return (
    <LiveButtonPrimitive
      render={<Button />}
      className={(state) => `media-live-button ${resolveClassName(className, state)}`}
      {...props}
    />
  );
}

interface VolumePopoverProps extends Omit<VolumePopoverPrimitive.RootProps, 'children'> {
  className?: ClassValue;
  orientation?: CoreVolumeSliderProps['orientation'];
  showTooltip?: boolean;
}

type MuteButtonProps = Omit<MuteButtonPrimitive.Props, 'children'>;

function MuteButton({ className, ...props }: MuteButtonProps = {}) {
  return (
    <MuteButtonPrimitive
      render={<Button />}
      className={(state) => `media-mute-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <VolumeOffIconPrimitive className={'media-button-icon media-mute-button-off-icon'} />
      <VolumeLowIconPrimitive className={'media-button-icon media-mute-button-low-icon'} />
      <VolumeHighIconPrimitive className={'media-button-icon media-mute-button-high-icon'} />
    </MuteButtonPrimitive>
  );
}

type VolumeSliderProps = Omit<VolumeSliderPrimitive.RootProps, 'children'>;

function VolumeSlider({ className, ...props }: VolumeSliderProps = {}) {
  return (
    <VolumeSliderPrimitive.Root
      className={(state) => `media-slider media-volume-slider ${resolveClassName(className, state)}`}
      thumbAlignment="edge"
      {...props}
    >
      <VolumeSliderPrimitive.Track render={<SliderTrack />}>
        <VolumeSliderPrimitive.Fill render={<SliderFill />} />
      </VolumeSliderPrimitive.Track>
      <VolumeSliderPrimitive.Thumb render={<SliderThumb />} className={'media-volume-slider-thumb'} />
    </VolumeSliderPrimitive.Root>
  );
}

interface CaptionsMenuProps extends Omit<MenuPrimitive.RootProps, 'children'> {
  className?: MenuPrimitive.TriggerProps['className'];
}

type CaptionsButtonProps = Omit<CaptionsButtonPrimitive.Props, 'children'>;

function CaptionsButton({ className, ...props }: CaptionsButtonProps = {}) {
  return (
    <CaptionsButtonPrimitive
      render={<Button />}
      className={(state) => `media-captions-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <CaptionsOffIconPrimitive className={'media-button-icon media-captions-button-off-icon'} />
      <CaptionsOnIconPrimitive className={'media-button-icon media-captions-button-on-icon'} />
    </CaptionsButtonPrimitive>
  );
}

type RadioItemProps = Menu.RadioItemProps;

function RadioItem({ children, className, ...props }: RadioItemProps) {
  return (
    <Menu.RadioItem className={(state) => `media-menu-radio-item ${resolveClassName(className, state)}`} {...props}>
      {children}
      <Menu.ItemIndicator forceMount className={'media-menu-item-indicator'}>
        <CheckIconPrimitive className={'media-menu-radio-item-icon'} />
      </Menu.ItemIndicator>
    </Menu.RadioItem>
  );
}

function CaptionsMenu({ className, ...props }: CaptionsMenuProps = {}) {
  return (
    <MenuPrimitive.Root side="top" align="center" boundary="viewport" {...props}>
      <CaptionsRadioGroup.Root>
        <ButtonTooltip side="top">
          <MenuPrimitive.Trigger render={<CaptionsButton />} className={className} />
        </ButtonTooltip>
        <MenuPrimitive.Popup className={'media-popup media-popup-surface media-menu-popup'}>
          <MenuPrimitive.Content className={'media-menu-content'}>
            <CaptionsRadioGroup.Options
              className={'media-menu-radio-group'}
              renderItem={(props, item) => (
                <RadioItem {...props}>
                  <span>{item.label}</span>
                </RadioItem>
              )}
            ></CaptionsRadioGroup.Options>
          </MenuPrimitive.Content>
        </MenuPrimitive.Popup>
      </CaptionsRadioGroup.Root>
    </MenuPrimitive.Root>
  );
}

type CastButtonProps = Omit<CastButtonPrimitive.Props, 'children'>;

function CastButton({ className, ...props }: CastButtonProps = {}) {
  return (
    <CastButtonPrimitive
      render={<Button />}
      className={(state) => `media-cast-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <CastEnterIconPrimitive className={'media-button-icon media-cast-button-enter-icon'} />
      <CastExitIconPrimitive className={'media-button-icon media-cast-button-exit-icon'} />
    </CastButtonPrimitive>
  );
}

type AirPlayButtonProps = Omit<AirPlayButtonPrimitive.Props, 'children'>;

function AirPlayButton({ className, ...props }: AirPlayButtonProps = {}) {
  return (
    <AirPlayButtonPrimitive
      render={<Button />}
      className={(state) => `media-airplay-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <AirPlayEnterIconPrimitive className={'media-button-icon media-airplay-button-enter-icon'} />
      <AirPlayExitIconPrimitive className={'media-button-icon media-airplay-button-exit-icon'} />
    </AirPlayButtonPrimitive>
  );
}

type PiPButtonProps = Omit<PiPButtonPrimitive.Props, 'children'>;

function PiPButton({ className, ...props }: PiPButtonProps = {}) {
  return (
    <PiPButtonPrimitive
      render={<Button />}
      className={(state) => `media-pip-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <PipEnterIconPrimitive className={'media-button-icon media-pip-button-enter-icon'} />
      <PipExitIconPrimitive className={'media-button-icon media-pip-button-exit-icon'} />
    </PiPButtonPrimitive>
  );
}

type FullscreenButtonProps = Omit<FullscreenButtonPrimitive.Props, 'children'>;

function FullscreenButton({ className, ...props }: FullscreenButtonProps = {}) {
  return (
    <FullscreenButtonPrimitive
      render={<Button />}
      className={(state) => `media-fullscreen-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <FullscreenEnterIconPrimitive className={'media-button-icon media-fullscreen-button-enter-icon'} />
      <FullscreenExitIconPrimitive className={'media-button-icon media-fullscreen-button-exit-icon'} />
    </FullscreenButtonPrimitive>
  );
}

function DefaultLiveVideoControls() {
  return (
    <Controls.Root>
      <Controls.Backdrop className={'video-controls-backdrop'} />
      <Controls.Content className={'video-controls video-controls-content video-controls-spaced'}>
        <Tooltip.Provider>
          <Controls.Group className={'video-controls-primary video-controls-spaced'}>
            <ButtonTooltip side="top">
              <PlayButton />
            </ButtonTooltip>
            <LiveButton />
            <div aria-hidden="true" className={'video-controls-spacer'} />
            <VolumePopover />
            <CaptionsMenu className={'video-controls-captions-menu'} />
            <ButtonTooltip side="top">
              <CastButton />
            </ButtonTooltip>
            <ButtonTooltip side="top">
              <AirPlayButton />
            </ButtonTooltip>
            <ButtonTooltip side="top">
              <PiPButton />
            </ButtonTooltip>
            <ButtonTooltip side="top">
              <FullscreenButton />
            </ButtonTooltip>
          </Controls.Group>
        </Tooltip.Provider>
      </Controls.Content>
    </Controls.Root>
  );
}

interface LiveVideoHotkeysProps {
  disabled?: boolean | undefined;
}

interface LivePlaybackHotkeysProps {
  disabled?: boolean | undefined;
}

function LivePlaybackHotkeys({ disabled = false }: LivePlaybackHotkeysProps = {}) {
  return (
    <>
      <Hotkey disabled={disabled} keys="Space" action="togglePaused" />
      <Hotkey disabled={disabled} keys="k" action="togglePaused" />
      <Hotkey disabled={disabled} keys="m" action="toggleMuted" />
      <Hotkey disabled={disabled} keys="ArrowUp" action="volumeStep" />
      <Hotkey disabled={disabled} keys="ArrowDown" action="volumeStep" />
    </>
  );
}

function LiveVideoHotkeys({ disabled = false }: LiveVideoHotkeysProps = {}) {
  return (
    <>
      <LivePlaybackHotkeys disabled={disabled} />
      <Hotkey disabled={disabled} keys="f" action="toggleFullscreen" />
      <Hotkey disabled={disabled} keys="c" action="toggleSubtitles" />
      <Hotkey disabled={disabled} keys="i" action="togglePictureInPicture" />
    </>
  );
}

interface LiveVideoGesturesProps {
  disabled?: boolean | undefined;
}

function LiveVideoGestures({ disabled = false }: LiveVideoGesturesProps = {}) {
  return (
    <>
      <Gesture disabled={disabled} type="tap" action="togglePaused" pointer="mouse" region="center" />
      <Gesture disabled={disabled} type="tap" action="toggleControls" pointer="touch" />
      <Gesture disabled={disabled} type="doubletap" action="toggleFullscreen" region="center" />
    </>
  );
}

type LiveVideoStatusIndicatorsProps = Omit<ComponentProps<'div'>, 'children'>;

type StatusAnnouncerProps = Omit<StatusAnnouncerPrimitive.Props, 'children'>;

function StatusAnnouncer({ className, ...props }: StatusAnnouncerProps = {}) {
  return (
    <StatusAnnouncerPrimitive
      className={(state) => `media-status-announcer ${resolveClassName(className, state)}`}
      {...props}
    />
  );
}

type VolumeIndicatorProps = Omit<VolumeIndicatorPrimitive.RootProps, 'children'>;

function VolumeIndicator({ className, ...props }: VolumeIndicatorProps = {}) {
  return (
    <VolumeIndicatorPrimitive.Root
      className={(state) => `media-indicator media-volume-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <VolumeIndicatorPrimitive.Fill className={'media-indicator-content media-volume-indicator-fill'}>
        <VolumeHighIconPrimitive className={'media-volume-indicator-high-icon'} />
        <VolumeLowIconPrimitive className={'media-volume-indicator-low-icon'} />
        <VolumeOffIconPrimitive className={'media-volume-indicator-off-icon'} />
        <VolumeIndicatorPrimitive.Value className={'media-volume-indicator-value'} />
      </VolumeIndicatorPrimitive.Fill>
    </VolumeIndicatorPrimitive.Root>
  );
}

type StatusIndicatorProps = Omit<StatusIndicatorPrimitive.RootProps, 'children'>;

const TOP_STATUS_ACTIONS = ['toggleSubtitles', 'toggleFullscreen', 'togglePictureInPicture'] as const;

function StatusIndicator({ className, ...props }: StatusIndicatorProps = {}) {
  return (
    <StatusIndicatorPrimitive.Root
      actions={TOP_STATUS_ACTIONS}
      className={(state) => `media-indicator media-status-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <div className={'media-indicator-content media-status-indicator-content'}>
        <CaptionsOnIconPrimitive className={'media-status-indicator-captions-on-icon'} />
        <CaptionsOffIconPrimitive className={'media-status-indicator-captions-off-icon'} />
        <FullscreenEnterIconPrimitive className={'media-status-indicator-fullscreen-enter-icon'} />
        <FullscreenExitIconPrimitive className={'media-status-indicator-fullscreen-exit-icon'} />
        <PipEnterIconPrimitive className={'media-status-indicator-pip-enter-icon'} />
        <PipExitIconPrimitive className={'media-status-indicator-pip-exit-icon'} />
        <StatusIndicatorPrimitive.Value className={'media-status-indicator-value'} />
      </div>
    </StatusIndicatorPrimitive.Root>
  );
}

type PlaybackStatusIndicatorProps = Omit<StatusIndicatorPrimitive.RootProps, 'children'>;

const PLAYBACK_STATUS_ACTIONS = ['togglePaused'] as const;

function PlaybackStatusIndicator({ className, ...props }: PlaybackStatusIndicatorProps = {}) {
  return (
    <StatusIndicatorPrimitive.Root
      actions={PLAYBACK_STATUS_ACTIONS}
      className={(state) => `media-playback-status-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <PlayIconPrimitive className={'media-playback-status-indicator-play-icon'} />
      <PauseIconPrimitive className={'media-playback-status-indicator-pause-icon'} />
    </StatusIndicatorPrimitive.Root>
  );
}

function LiveVideoStatusIndicators({ className, ...props }: LiveVideoStatusIndicatorsProps = {}) {
  return (
    <>
      <StatusAnnouncer />
      <div className={`video-status-indicators ${className ?? ''}`} {...props}>
        <VolumeIndicator />
        <StatusIndicator />
        <PlaybackStatusIndicator />
      </div>
    </>
  );
}

function DefaultLiveVideoSkin({ children, className, renderPoster, ...props }: DefaultLiveVideoSkinProps = {}) {
  return (
    <Container className={`video-skin ${className ?? ''}`} data-theme="default" data-preset="live-video" {...props}>
      {children}
      <Poster>{renderPoster}</Poster>
      <BufferingIndicator />
      <ErrorDialog />
      <DefaultLiveVideoControls />
      <LiveVideoHotkeys />
      <LiveVideoGestures />
      <LiveVideoStatusIndicators />
    </Container>
  );
}

const Skin = DefaultLiveVideoSkin;

// ================================================================
// Player
// ================================================================

export interface LiveVideoPlayerProps {
  children?: ReactNode;
  style?: CSSProperties;
  className?: string;
  renderPoster?: Poster.Props['render'];
}

export function LiveVideoSkin(props: LiveVideoSkinProps) {
  return <Skin {...props} />;
}

// ================================================================
// Components
// ================================================================

function Button({ className, ...props }: ButtonProps) {
  return <button className={`media-button ${className ?? ''}`} {...props} />;
}

type SliderTrackProps = ComponentProps<'div'>;

function SliderTrack({ className, ...props }: SliderTrackProps) {
  return <div className={`media-slider-track ${className ?? ''}`} {...props} />;
}

type SliderFillProps = ComponentProps<'div'>;

function SliderFill({ className, ...props }: SliderFillProps) {
  return <div className={`media-slider-fill ${className ?? ''}`} {...props} />;
}

type SliderThumbProps = ComponentProps<'div'>;

function SliderThumb({ className, ...props }: SliderThumbProps) {
  return <div className={`media-slider-thumb ${className ?? ''}`} {...props} />;
}

function VolumePopover({
  className,
  showTooltip = false,
  side = 'top',
  orientation = 'vertical',
  ...props
}: VolumePopoverProps = {}) {
  return (
    <VolumePopoverPrimitive.Root openOnHover delay={200} closeDelay={100} side={side} {...props}>
      <ButtonTooltip delay={0} disabled={!showTooltip} sticky side="top">
        <VolumePopoverPrimitive.Trigger render={<MuteButton className={`${className ?? ''}`} />} />
      </ButtonTooltip>
      <VolumePopoverPrimitive.Popup
        className={'media-popup media-popup-safe-area media-popup-transition media-popup-surface media-volume-popover'}
      >
        <VolumeSlider orientation={orientation} />
      </VolumePopoverPrimitive.Popup>
    </VolumePopoverPrimitive.Root>
  );
}

// ================================================================
// Error Dialog
// ================================================================

function ErrorDialog() {
  return (
    <ErrorDialogPrimitive.Root>
      <ErrorDialogPrimitive.Backdrop className={'media-dialog-backdrop'} />
      <ErrorDialogPrimitive.Popup className={'media-dialog-popup'}>
        <div className={'media-dialog-content'}>
          <ErrorDialogPrimitive.Title className={'media-dialog-title'} />
          <ErrorDialogPrimitive.Description className={'media-dialog-description'} />
        </div>
        <div className={'media-dialog-actions'}>
          <ErrorDialogPrimitive.Close render={<Button />} className={'media-dialog-close'} />
        </div>
      </ErrorDialogPrimitive.Popup>
    </ErrorDialogPrimitive.Root>
  );
}

Default live audio skin

'use client';

import { StatusAnnouncer as StatusAnnouncerPrimitive, Container as ContainerPrimitive, ErrorDialog, Hotkey, LiveButton as LiveButtonPrimitive, MuteButton as MuteButtonPrimitive, Tooltip, VolumeSlider as VolumeSliderPrimitive, VolumePopover as VolumePopoverPrimitive, PlayButton as PlayButtonPrimitive, BufferingIndicator as BufferingIndicatorPrimitive, Controls } from '@videojs/react';
import { MuxAudio } from '@videojs/react/media/mux-audio';
import './player.css';
import { Player } from './player';
import { type ComponentProps, type SVGProps, type ReactElement, type ReactNode, type CSSProperties } from 'react';
import { VolumeOffIcon as VolumeOffIconPrimitive, VolumeLowIcon as VolumeLowIconPrimitive, VolumeHighIcon as VolumeHighIconPrimitive, RestartIcon as RestartIconPrimitive, PlayIcon as PlayIconPrimitive, PauseIcon as PauseIconPrimitive, SpinnerIcon as SpinnerIconPrimitive } from '@videojs/react/icons';
import { type VolumeSliderProps as CoreVolumeSliderProps } from '@videojs/core';
import { type Poster } from '@videojs/react';

type ContainerProps = ContainerPrimitive.Props;

function Container({ children, className, ...props }: ContainerProps) {
  return (
    <ContainerPrimitive className={`media-skin media-container ${className ?? ''}`} {...props}>
      {children}
    </ContainerPrimitive>
  );
}

interface DefaultLiveAudioSkinProps extends Omit<NonNullable<ComponentProps<typeof Container>>, 'children'> {
  src: string;
}

type ButtonProps = ComponentProps<'button'>;

function AudioErrorDialog() {
  return (
    <ErrorDialog.Root>
      <ErrorDialog.Backdrop className={'audio-dialog-backdrop'} />
      <ErrorDialog.Popup className={'audio-dialog-popup'}>
        <div className={'audio-dialog-content'}>
          <ErrorDialog.Title className={'audio-dialog-title'} />
          <ErrorDialog.Description className={'audio-dialog-description'} />
        </div>
        <div className={'audio-dialog-actions'}>
          <ErrorDialog.Close render={<Button />} className={'audio-dialog-close'} />
        </div>
      </ErrorDialog.Popup>
    </ErrorDialog.Root>
  );
}

type AudioPlayButtonProps = Omit<ComponentProps<'div'>, 'children'>;

type BufferingIndicatorProps = Omit<BufferingIndicatorPrimitive.Props, 'children'>;

function BufferingIndicator({ className, ...props }: BufferingIndicatorProps = {}) {
  return (
    <BufferingIndicatorPrimitive
      className={(state) => `media-buffering-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <SpinnerIconPrimitive className={'media-buffering-indicator-spinner-icon'} />
    </BufferingIndicatorPrimitive>
  );
}

interface ButtonTooltipProps extends Tooltip.RootProps {
  children: ReactElement;
  label?: ReactNode;
}

function ButtonTooltip({ children, label, ...props }: ButtonTooltipProps) {
  return (
    <Tooltip.Root {...props}>
      <Tooltip.Trigger render={children} />
      <Tooltip.Popup
        className={'media-popup media-popup-safe-area media-popup-transition media-popup-surface media-tooltip'}
      >
        {label ?? <Tooltip.Label />}
        {!label && <Tooltip.Shortcut className={'media-tooltip-shortcut'} />}
      </Tooltip.Popup>
    </Tooltip.Root>
  );
}

type PlayButtonProps = Omit<PlayButtonPrimitive.Props, 'children'>;

function PlayButton({ className, ...props }: PlayButtonProps = {}) {
  return (
    <PlayButtonPrimitive
      render={<Button />}
      className={(state) => `media-play-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <RestartIconPrimitive className={'media-button-icon media-play-button-restart-icon'} />
      <PlayIconPrimitive className={'media-button-icon media-play-button-play-icon'} />
      <PauseIconPrimitive className={'media-button-icon media-play-button-pause-icon'} />
    </PlayButtonPrimitive>
  );
}

function AudioPlayButton({ className, ...props }: AudioPlayButtonProps = {}) {
  return (
    <div className={`audio-play-button ${className ?? ''}`} {...props}>
      <BufferingIndicator className={'audio-play-button-buffering-indicator'} />
      <ButtonTooltip boundary="viewport" side="top">
        <PlayButton />
      </ButtonTooltip>
    </div>
  );
}

type LiveButtonProps = Omit<LiveButtonPrimitive.Props, 'children'>;

function LiveButton({ className, ...props }: LiveButtonProps = {}) {
  return (
    <LiveButtonPrimitive
      render={<Button />}
      className={(state) => `media-live-button ${resolveClassName(className, state)}`}
      {...props}
    />
  );
}

interface VolumePopoverProps extends Omit<VolumePopoverPrimitive.RootProps, 'children'> {
  className?: ClassValue;
  orientation?: CoreVolumeSliderProps['orientation'];
  showTooltip?: boolean;
}

type MuteButtonProps = Omit<MuteButtonPrimitive.Props, 'children'>;

function MuteButton({ className, ...props }: MuteButtonProps = {}) {
  return (
    <MuteButtonPrimitive
      render={<Button />}
      className={(state) => `media-mute-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <VolumeOffIconPrimitive className={'media-button-icon media-mute-button-off-icon'} />
      <VolumeLowIconPrimitive className={'media-button-icon media-mute-button-low-icon'} />
      <VolumeHighIconPrimitive className={'media-button-icon media-mute-button-high-icon'} />
    </MuteButtonPrimitive>
  );
}

type VolumeSliderProps = Omit<VolumeSliderPrimitive.RootProps, 'children'>;

function VolumeSlider({ className, ...props }: VolumeSliderProps = {}) {
  return (
    <VolumeSliderPrimitive.Root
      className={(state) => `media-slider media-volume-slider ${resolveClassName(className, state)}`}
      thumbAlignment="edge"
      {...props}
    >
      <VolumeSliderPrimitive.Track render={<SliderTrack />}>
        <VolumeSliderPrimitive.Fill render={<SliderFill />} />
      </VolumeSliderPrimitive.Track>
      <VolumeSliderPrimitive.Thumb render={<SliderThumb />} className={'media-volume-slider-thumb'} />
    </VolumeSliderPrimitive.Root>
  );
}

function DefaultLiveAudioControls() {
  return (
    <Controls.Root visibility="always">
      <Controls.Content className={'audio-controls audio-controls-content'}>
        <Tooltip.Provider>
          <Controls.Group className={'audio-controls-start'}>
            <AudioPlayButton />
            <LiveButton />
          </Controls.Group>

          <div aria-hidden="true" className={'audio-controls-spacer'} />

          <Controls.Group className={'audio-controls-end'}>
            <VolumePopover boundary="viewport" />
          </Controls.Group>
        </Tooltip.Provider>
      </Controls.Content>
    </Controls.Root>
  );
}

interface LivePlaybackHotkeysProps {
  disabled?: boolean | undefined;
}

function LivePlaybackHotkeys({ disabled = false }: LivePlaybackHotkeysProps = {}) {
  return (
    <>
      <Hotkey disabled={disabled} keys="Space" action="togglePaused" />
      <Hotkey disabled={disabled} keys="k" action="togglePaused" />
      <Hotkey disabled={disabled} keys="m" action="toggleMuted" />
      <Hotkey disabled={disabled} keys="ArrowUp" action="volumeStep" />
      <Hotkey disabled={disabled} keys="ArrowDown" action="volumeStep" />
    </>
  );
}

type StatusAnnouncerProps = Omit<StatusAnnouncerPrimitive.Props, 'children'>;

function StatusAnnouncer({ className, ...props }: StatusAnnouncerProps = {}) {
  return (
    <StatusAnnouncerPrimitive
      className={(state) => `media-status-announcer ${resolveClassName(className, state)}`}
      {...props}
    />
  );
}

function DefaultLiveAudioSkin({ children, className, ...props }: DefaultLiveAudioSkinProps = {}) {
  return (
    <Container className={`audio-skin ${className ?? ''}`} data-theme="default" data-preset="live-audio" {...props}>
      {children}
      <AudioErrorDialog />
      <DefaultLiveAudioControls />
      <LivePlaybackHotkeys />
      <StatusAnnouncer />
    </Container>
  );
}

const Skin = DefaultLiveAudioSkin;

// ================================================================
// Player
// ================================================================

export interface LiveAudioPlayerProps {
  children?: ReactNode;
  style?: CSSProperties;
  className?: string;
}

export function LiveAudioSkin(props: LiveAudioSkinProps) {
  return <Skin {...props} />;
}

// ================================================================
// Components
// ================================================================

function Button({ className, ...props }: ButtonProps) {
  return <button className={`media-button ${className ?? ''}`} {...props} />;
}

type SliderTrackProps = ComponentProps<'div'>;

function SliderTrack({ className, ...props }: SliderTrackProps) {
  return <div className={`media-slider-track ${className ?? ''}`} {...props} />;
}

type SliderFillProps = ComponentProps<'div'>;

function SliderFill({ className, ...props }: SliderFillProps) {
  return <div className={`media-slider-fill ${className ?? ''}`} {...props} />;
}

type SliderThumbProps = ComponentProps<'div'>;

function SliderThumb({ className, ...props }: SliderThumbProps) {
  return <div className={`media-slider-thumb ${className ?? ''}`} {...props} />;
}

function VolumePopover({
  className,
  showTooltip = false,
  side = 'top',
  orientation = 'vertical',
  ...props
}: VolumePopoverProps = {}) {
  return (
    <VolumePopoverPrimitive.Root openOnHover delay={200} closeDelay={100} side={side} {...props}>
      <ButtonTooltip delay={0} disabled={!showTooltip} sticky side="top">
        <VolumePopoverPrimitive.Trigger render={<MuteButton className={`${className ?? ''}`} />} />
      </ButtonTooltip>
      <VolumePopoverPrimitive.Popup
        className={'media-popup media-popup-safe-area media-popup-transition media-popup-surface media-volume-popover'}
      >
        <VolumeSlider orientation={orientation} />
      </VolumePopoverPrimitive.Popup>
    </VolumePopoverPrimitive.Root>
  );
}

Minimal live video skin

'use client';

import { type ComponentProps, type SVGProps, type ReactElement, type ReactNode, type CSSProperties } from 'react';
import { ErrorDialog as ErrorDialogPrimitive, Container as ContainerPrimitive, Poster as PosterPrimitive, Gesture, Hotkey, StatusIndicator as StatusIndicatorPrimitive, VolumeIndicator as VolumeIndicatorPrimitive, StatusAnnouncer as StatusAnnouncerPrimitive, BufferingIndicator as BufferingIndicatorPrimitive, Tooltip, LiveButton as LiveButtonPrimitive, MuteButton as MuteButtonPrimitive, VolumeSlider as VolumeSliderPrimitive, VolumePopover as VolumePopoverPrimitive, CaptionsButton as CaptionsButtonPrimitive, Menu, CaptionsRadioGroup, AirPlayButton as AirPlayButtonPrimitive, CastButton as CastButtonPrimitive, FullscreenButton as FullscreenButtonPrimitive, PiPButton as PiPButtonPrimitive, PlayButton as PlayButtonPrimitive, Controls } from '@videojs/react';
import { HlsJsVideo } from '@videojs/react/media/hlsjs-video';
import './player.css';
import { Player } from './player';
import { CaptionsOnIcon as CaptionsOnIconPrimitive, CaptionsOffIcon as CaptionsOffIconPrimitive, FullscreenEnterIcon as FullscreenEnterIconPrimitive, FullscreenExitIcon as FullscreenExitIconPrimitive, PipEnterIcon as PipEnterIconPrimitive, PipExitIcon as PipExitIconPrimitive, PlayIcon as PlayIconPrimitive, PauseIcon as PauseIconPrimitive, VolumeHighIcon as VolumeHighIconPrimitive, VolumeLowIcon as VolumeLowIconPrimitive, VolumeOffIcon as VolumeOffIconPrimitive, CheckIcon as CheckIconPrimitive } from '@videojs/react/icons';
import { SpinnerIcon as SpinnerIconPrimitive, AirPlayEnterIcon as AirPlayEnterIconPrimitive, AirPlayExitIcon as AirPlayExitIconPrimitive, CastEnterIcon as CastEnterIconPrimitive, CastExitIcon as CastExitIconPrimitive, FullscreenEnterIcon as FullscreenEnterIconPrimitive, FullscreenExitIcon as FullscreenExitIconPrimitive, PipEnterIcon as PipEnterIconPrimitive, PipExitIcon as PipExitIconPrimitive, RestartIcon as RestartIconPrimitive, PlayIcon as PlayIconPrimitive, PauseIcon as PauseIconPrimitive } from '@videojs/react/icons/minimal';
import { type VolumeSliderProps as CoreVolumeSliderProps } from '@videojs/core';
import { type Poster } from '@videojs/react';

type ContainerProps = ContainerPrimitive.Props;

function Container({ children, className, ...props }: ContainerProps) {
  return (
    <ContainerPrimitive className={`media-skin media-container ${className ?? ''}`} {...props}>
      {children}
    </ContainerPrimitive>
  );
}

interface PosterProps extends Omit<PosterPrimitive.Props, 'children' | 'render'> {
  children?: PosterPrimitive.Props['render'];
}

function Poster({ children, className, src, ...props }: PosterProps = {}) {
  return (
    <PosterPrimitive
      render={children}
      className={(state) => `media-poster ${resolveClassName(className, state)}`}
      src={src}
      {...props}
    />
  );
}

interface MinimalLiveVideoSkinProps extends Omit<NonNullable<ComponentProps<typeof Container>>, 'children'> {
  src: string;
  poster?: string | undefined;
  renderPoster?: NonNullable<ComponentProps<typeof Poster>>['children'];
}

type BufferingIndicatorProps = Omit<BufferingIndicatorPrimitive.Props, 'children'>;

function BufferingIndicator({ className, ...props }: BufferingIndicatorProps = {}) {
  return (
    <BufferingIndicatorPrimitive
      className={(state) => `media-buffering-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <SpinnerIconPrimitive className={'media-buffering-indicator-spinner-icon'} />
    </BufferingIndicatorPrimitive>
  );
}

type ButtonProps = ComponentProps<'button'>;

interface ButtonTooltipProps extends Tooltip.RootProps {
  children: ReactElement;
  label?: ReactNode;
}

function ButtonTooltip({ children, label, ...props }: ButtonTooltipProps) {
  return (
    <Tooltip.Root {...props}>
      <Tooltip.Trigger render={children} />
      <Tooltip.Popup
        className={'media-popup media-popup-safe-area media-popup-transition media-popup-surface media-tooltip'}
      >
        {label ?? <Tooltip.Label />}
        {!label && <Tooltip.Shortcut className={'media-tooltip-shortcut'} />}
      </Tooltip.Popup>
    </Tooltip.Root>
  );
}

type PlayButtonProps = Omit<PlayButtonPrimitive.Props, 'children'>;

function PlayButton({ className, ...props }: PlayButtonProps = {}) {
  return (
    <PlayButtonPrimitive
      render={<Button />}
      className={(state) => `media-play-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <RestartIconPrimitive className={'media-button-icon media-play-button-restart-icon'} />
      <PlayIconPrimitive className={'media-button-icon media-play-button-play-icon'} />
      <PauseIconPrimitive className={'media-button-icon media-play-button-pause-icon'} />
    </PlayButtonPrimitive>
  );
}

type LiveButtonProps = Omit<LiveButtonPrimitive.Props, 'children'>;

function LiveButton({ className, ...props }: LiveButtonProps = {}) {
  return (
    <LiveButtonPrimitive
      render={<Button />}
      className={(state) => `media-live-button ${resolveClassName(className, state)}`}
      {...props}
    />
  );
}

interface VolumePopoverProps extends Omit<VolumePopoverPrimitive.RootProps, 'children'> {
  className?: ClassValue;
  orientation?: CoreVolumeSliderProps['orientation'];
  showTooltip?: boolean;
}

type MuteButtonProps = Omit<MuteButtonPrimitive.Props, 'children'>;

function MuteButton({ className, ...props }: MuteButtonProps = {}) {
  return (
    <MuteButtonPrimitive
      render={<Button />}
      className={(state) => `media-mute-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <VolumeOffIconPrimitive className={'media-button-icon media-mute-button-off-icon'} />
      <VolumeLowIconPrimitive className={'media-button-icon media-mute-button-low-icon'} />
      <VolumeHighIconPrimitive className={'media-button-icon media-mute-button-high-icon'} />
    </MuteButtonPrimitive>
  );
}

type VolumeSliderProps = Omit<VolumeSliderPrimitive.RootProps, 'children'>;

function VolumeSlider({ className, ...props }: VolumeSliderProps = {}) {
  return (
    <VolumeSliderPrimitive.Root
      className={(state) => `media-slider media-volume-slider ${resolveClassName(className, state)}`}
      thumbAlignment="edge"
      {...props}
    >
      <VolumeSliderPrimitive.Track render={<SliderTrack />}>
        <VolumeSliderPrimitive.Fill render={<SliderFill />} />
      </VolumeSliderPrimitive.Track>
      <VolumeSliderPrimitive.Thumb render={<SliderThumb />} className={'media-volume-slider-thumb'} />
    </VolumeSliderPrimitive.Root>
  );
}

interface CaptionsMenuProps extends Omit<MenuPrimitive.RootProps, 'children'> {
  className?: MenuPrimitive.TriggerProps['className'];
}

type CaptionsButtonProps = Omit<CaptionsButtonPrimitive.Props, 'children'>;

function CaptionsButton({ className, ...props }: CaptionsButtonProps = {}) {
  return (
    <CaptionsButtonPrimitive
      render={<Button />}
      className={(state) => `media-captions-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <CaptionsOffIconPrimitive className={'media-button-icon media-captions-button-off-icon'} />
      <CaptionsOnIconPrimitive className={'media-button-icon media-captions-button-on-icon'} />
    </CaptionsButtonPrimitive>
  );
}

type RadioItemProps = Menu.RadioItemProps;

function RadioItem({ children, className, ...props }: RadioItemProps) {
  return (
    <Menu.RadioItem className={(state) => `media-menu-radio-item ${resolveClassName(className, state)}`} {...props}>
      {children}
      <Menu.ItemIndicator forceMount className={'media-menu-item-indicator'}>
        <CheckIconPrimitive className={'media-menu-radio-item-icon'} />
      </Menu.ItemIndicator>
    </Menu.RadioItem>
  );
}

function CaptionsMenu({ className, ...props }: CaptionsMenuProps = {}) {
  return (
    <MenuPrimitive.Root side="top" align="center" boundary="viewport" {...props}>
      <CaptionsRadioGroup.Root>
        <ButtonTooltip side="top">
          <MenuPrimitive.Trigger render={<CaptionsButton />} className={className} />
        </ButtonTooltip>
        <MenuPrimitive.Popup className={'media-popup media-popup-surface media-menu-popup'}>
          <MenuPrimitive.Content className={'media-menu-content'}>
            <CaptionsRadioGroup.Options
              className={'media-menu-radio-group'}
              renderItem={(props, item) => (
                <RadioItem {...props}>
                  <span>{item.label}</span>
                </RadioItem>
              )}
            ></CaptionsRadioGroup.Options>
          </MenuPrimitive.Content>
        </MenuPrimitive.Popup>
      </CaptionsRadioGroup.Root>
    </MenuPrimitive.Root>
  );
}

type CastButtonProps = Omit<CastButtonPrimitive.Props, 'children'>;

function CastButton({ className, ...props }: CastButtonProps = {}) {
  return (
    <CastButtonPrimitive
      render={<Button />}
      className={(state) => `media-cast-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <CastEnterIconPrimitive className={'media-button-icon media-cast-button-enter-icon'} />
      <CastExitIconPrimitive className={'media-button-icon media-cast-button-exit-icon'} />
    </CastButtonPrimitive>
  );
}

type AirPlayButtonProps = Omit<AirPlayButtonPrimitive.Props, 'children'>;

function AirPlayButton({ className, ...props }: AirPlayButtonProps = {}) {
  return (
    <AirPlayButtonPrimitive
      render={<Button />}
      className={(state) => `media-airplay-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <AirPlayEnterIconPrimitive className={'media-button-icon media-airplay-button-enter-icon'} />
      <AirPlayExitIconPrimitive className={'media-button-icon media-airplay-button-exit-icon'} />
    </AirPlayButtonPrimitive>
  );
}

type PiPButtonProps = Omit<PiPButtonPrimitive.Props, 'children'>;

function PiPButton({ className, ...props }: PiPButtonProps = {}) {
  return (
    <PiPButtonPrimitive
      render={<Button />}
      className={(state) => `media-pip-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <PipEnterIconPrimitive className={'media-button-icon media-pip-button-enter-icon'} />
      <PipExitIconPrimitive className={'media-button-icon media-pip-button-exit-icon'} />
    </PiPButtonPrimitive>
  );
}

type FullscreenButtonProps = Omit<FullscreenButtonPrimitive.Props, 'children'>;

function FullscreenButton({ className, ...props }: FullscreenButtonProps = {}) {
  return (
    <FullscreenButtonPrimitive
      render={<Button />}
      className={(state) => `media-fullscreen-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <FullscreenEnterIconPrimitive className={'media-button-icon media-fullscreen-button-enter-icon'} />
      <FullscreenExitIconPrimitive className={'media-button-icon media-fullscreen-button-exit-icon'} />
    </FullscreenButtonPrimitive>
  );
}

function MinimalLiveVideoControls() {
  return (
    <Controls.Root>
      <Controls.Backdrop className={'video-controls-backdrop'} />
      <Controls.Content className={'video-controls video-controls-content'}>
        <Tooltip.Provider>
          <Controls.Group className={'video-controls-start'}>
            <ButtonTooltip side="top">
              <PlayButton />
            </ButtonTooltip>
            <LiveButton />
            <VolumePopover showTooltip side="right" orientation="horizontal" />
          </Controls.Group>

          <div aria-hidden="true" className={'video-controls-spacer'} />

          <Controls.Group className={'video-controls-end'}>
            <CaptionsMenu />
            <ButtonTooltip side="top">
              <CastButton />
            </ButtonTooltip>
            <ButtonTooltip side="top">
              <AirPlayButton />
            </ButtonTooltip>
            <ButtonTooltip side="top">
              <PiPButton />
            </ButtonTooltip>
            <ButtonTooltip side="top">
              <FullscreenButton />
            </ButtonTooltip>
          </Controls.Group>
        </Tooltip.Provider>
      </Controls.Content>
    </Controls.Root>
  );
}

interface LiveVideoHotkeysProps {
  disabled?: boolean | undefined;
}

interface LivePlaybackHotkeysProps {
  disabled?: boolean | undefined;
}

function LivePlaybackHotkeys({ disabled = false }: LivePlaybackHotkeysProps = {}) {
  return (
    <>
      <Hotkey disabled={disabled} keys="Space" action="togglePaused" />
      <Hotkey disabled={disabled} keys="k" action="togglePaused" />
      <Hotkey disabled={disabled} keys="m" action="toggleMuted" />
      <Hotkey disabled={disabled} keys="ArrowUp" action="volumeStep" />
      <Hotkey disabled={disabled} keys="ArrowDown" action="volumeStep" />
    </>
  );
}

function LiveVideoHotkeys({ disabled = false }: LiveVideoHotkeysProps = {}) {
  return (
    <>
      <LivePlaybackHotkeys disabled={disabled} />
      <Hotkey disabled={disabled} keys="f" action="toggleFullscreen" />
      <Hotkey disabled={disabled} keys="c" action="toggleSubtitles" />
      <Hotkey disabled={disabled} keys="i" action="togglePictureInPicture" />
    </>
  );
}

interface LiveVideoGesturesProps {
  disabled?: boolean | undefined;
}

function LiveVideoGestures({ disabled = false }: LiveVideoGesturesProps = {}) {
  return (
    <>
      <Gesture disabled={disabled} type="tap" action="togglePaused" pointer="mouse" region="center" />
      <Gesture disabled={disabled} type="tap" action="toggleControls" pointer="touch" />
      <Gesture disabled={disabled} type="doubletap" action="toggleFullscreen" region="center" />
    </>
  );
}

type LiveVideoStatusIndicatorsProps = Omit<ComponentProps<'div'>, 'children'>;

type StatusAnnouncerProps = Omit<StatusAnnouncerPrimitive.Props, 'children'>;

function StatusAnnouncer({ className, ...props }: StatusAnnouncerProps = {}) {
  return (
    <StatusAnnouncerPrimitive
      className={(state) => `media-status-announcer ${resolveClassName(className, state)}`}
      {...props}
    />
  );
}

type VolumeIndicatorProps = Omit<VolumeIndicatorPrimitive.RootProps, 'children'>;

function VolumeIndicator({ className, ...props }: VolumeIndicatorProps = {}) {
  return (
    <VolumeIndicatorPrimitive.Root
      className={(state) => `media-indicator media-volume-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <VolumeIndicatorPrimitive.Fill className={'media-indicator-content media-volume-indicator-fill'}>
        <VolumeHighIconPrimitive className={'media-volume-indicator-high-icon'} />
        <VolumeLowIconPrimitive className={'media-volume-indicator-low-icon'} />
        <VolumeOffIconPrimitive className={'media-volume-indicator-off-icon'} />
        <VolumeIndicatorPrimitive.Value className={'media-volume-indicator-value'} />
      </VolumeIndicatorPrimitive.Fill>
    </VolumeIndicatorPrimitive.Root>
  );
}

type StatusIndicatorProps = Omit<StatusIndicatorPrimitive.RootProps, 'children'>;

const TOP_STATUS_ACTIONS = ['toggleSubtitles', 'toggleFullscreen', 'togglePictureInPicture'] as const;

function StatusIndicator({ className, ...props }: StatusIndicatorProps = {}) {
  return (
    <StatusIndicatorPrimitive.Root
      actions={TOP_STATUS_ACTIONS}
      className={(state) => `media-indicator media-status-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <div className={'media-indicator-content media-status-indicator-content'}>
        <CaptionsOnIconPrimitive className={'media-status-indicator-captions-on-icon'} />
        <CaptionsOffIconPrimitive className={'media-status-indicator-captions-off-icon'} />
        <FullscreenEnterIconPrimitive className={'media-status-indicator-fullscreen-enter-icon'} />
        <FullscreenExitIconPrimitive className={'media-status-indicator-fullscreen-exit-icon'} />
        <PipEnterIconPrimitive className={'media-status-indicator-pip-enter-icon'} />
        <PipExitIconPrimitive className={'media-status-indicator-pip-exit-icon'} />
        <StatusIndicatorPrimitive.Value className={'media-status-indicator-value'} />
      </div>
    </StatusIndicatorPrimitive.Root>
  );
}

type PlaybackStatusIndicatorProps = Omit<StatusIndicatorPrimitive.RootProps, 'children'>;

const PLAYBACK_STATUS_ACTIONS = ['togglePaused'] as const;

function PlaybackStatusIndicator({ className, ...props }: PlaybackStatusIndicatorProps = {}) {
  return (
    <StatusIndicatorPrimitive.Root
      actions={PLAYBACK_STATUS_ACTIONS}
      className={(state) => `media-playback-status-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <PlayIconPrimitive className={'media-playback-status-indicator-play-icon'} />
      <PauseIconPrimitive className={'media-playback-status-indicator-pause-icon'} />
    </StatusIndicatorPrimitive.Root>
  );
}

function LiveVideoStatusIndicators({ className, ...props }: LiveVideoStatusIndicatorsProps = {}) {
  return (
    <>
      <StatusAnnouncer />
      <div className={`video-status-indicators ${className ?? ''}`} {...props}>
        <VolumeIndicator />
        <StatusIndicator />
        <PlaybackStatusIndicator />
      </div>
    </>
  );
}

function MinimalLiveVideoSkin({ children, className, renderPoster, ...props }: MinimalLiveVideoSkinProps = {}) {
  return (
    <Container className={`video-skin ${className ?? ''}`} data-theme="minimal" data-preset="live-video" {...props}>
      {children}
      <Poster>{renderPoster}</Poster>
      <BufferingIndicator />
      <ErrorDialog />
      <MinimalLiveVideoControls />
      <LiveVideoHotkeys />
      <LiveVideoGestures />
      <LiveVideoStatusIndicators />
    </Container>
  );
}

const Skin = MinimalLiveVideoSkin;

// ================================================================
// Player
// ================================================================

export interface LiveVideoPlayerProps {
  children?: ReactNode;
  style?: CSSProperties;
  className?: string;
  renderPoster?: Poster.Props['render'];
}

export function MinimalLiveVideoSkin(props: MinimalLiveVideoSkinProps) {
  return <Skin {...props} />;
}

// ================================================================
// Components
// ================================================================

function Button({ className, ...props }: ButtonProps) {
  return <button className={`media-button ${className ?? ''}`} {...props} />;
}

type SliderTrackProps = ComponentProps<'div'>;

function SliderTrack({ className, ...props }: SliderTrackProps) {
  return <div className={`media-slider-track ${className ?? ''}`} {...props} />;
}

type SliderFillProps = ComponentProps<'div'>;

function SliderFill({ className, ...props }: SliderFillProps) {
  return <div className={`media-slider-fill ${className ?? ''}`} {...props} />;
}

type SliderThumbProps = ComponentProps<'div'>;

function SliderThumb({ className, ...props }: SliderThumbProps) {
  return <div className={`media-slider-thumb ${className ?? ''}`} {...props} />;
}

function VolumePopover({
  className,
  showTooltip = false,
  side = 'top',
  orientation = 'vertical',
  ...props
}: VolumePopoverProps = {}) {
  return (
    <VolumePopoverPrimitive.Root openOnHover delay={200} closeDelay={100} side={side} {...props}>
      <ButtonTooltip delay={0} disabled={!showTooltip} sticky side="top">
        <VolumePopoverPrimitive.Trigger render={<MuteButton className={`${className ?? ''}`} />} />
      </ButtonTooltip>
      <VolumePopoverPrimitive.Popup
        className={'media-popup media-popup-safe-area media-popup-transition media-popup-surface media-volume-popover'}
      >
        <VolumeSlider orientation={orientation} />
      </VolumePopoverPrimitive.Popup>
    </VolumePopoverPrimitive.Root>
  );
}

// ================================================================
// Error Dialog
// ================================================================

function ErrorDialog() {
  return (
    <ErrorDialogPrimitive.Root>
      <ErrorDialogPrimitive.Backdrop className={'media-dialog-backdrop'} />
      <ErrorDialogPrimitive.Popup className={'media-dialog-popup'}>
        <div className={'media-dialog-content'}>
          <ErrorDialogPrimitive.Title className={'media-dialog-title'} />
          <ErrorDialogPrimitive.Description className={'media-dialog-description'} />
        </div>
        <div className={'media-dialog-actions'}>
          <ErrorDialogPrimitive.Close render={<Button />} className={'media-dialog-close'} />
        </div>
      </ErrorDialogPrimitive.Popup>
    </ErrorDialogPrimitive.Root>
  );
}

Minimal live audio skin

'use client';

import { StatusAnnouncer as StatusAnnouncerPrimitive, Container as ContainerPrimitive, ErrorDialog, Hotkey, LiveButton as LiveButtonPrimitive, MuteButton as MuteButtonPrimitive, Tooltip, VolumeSlider as VolumeSliderPrimitive, VolumePopover as VolumePopoverPrimitive, PlayButton as PlayButtonPrimitive, BufferingIndicator as BufferingIndicatorPrimitive, Controls } from '@videojs/react';
import { MuxAudio } from '@videojs/react/media/mux-audio';
import './player.css';
import { Player } from './player';
import { type ComponentProps, type SVGProps, type ReactElement, type ReactNode, type CSSProperties } from 'react';
import { VolumeOffIcon as VolumeOffIconPrimitive, VolumeLowIcon as VolumeLowIconPrimitive, VolumeHighIcon as VolumeHighIconPrimitive, RestartIcon as RestartIconPrimitive, PlayIcon as PlayIconPrimitive, PauseIcon as PauseIconPrimitive, SpinnerIcon as SpinnerIconPrimitive } from '@videojs/react/icons';
import { type VolumeSliderProps as CoreVolumeSliderProps } from '@videojs/core';
import { type Poster } from '@videojs/react';

type ContainerProps = ContainerPrimitive.Props;

function Container({ children, className, ...props }: ContainerProps) {
  return (
    <ContainerPrimitive className={`media-skin media-container ${className ?? ''}`} {...props}>
      {children}
    </ContainerPrimitive>
  );
}

interface MinimalLiveAudioSkinProps extends Omit<NonNullable<ComponentProps<typeof Container>>, 'children'> {
  src: string;
}

type ButtonProps = ComponentProps<'button'>;

function AudioErrorDialog() {
  return (
    <ErrorDialog.Root>
      <ErrorDialog.Backdrop className={'audio-dialog-backdrop'} />
      <ErrorDialog.Popup className={'audio-dialog-popup'}>
        <div className={'audio-dialog-content'}>
          <ErrorDialog.Title className={'audio-dialog-title'} />
          <ErrorDialog.Description className={'audio-dialog-description'} />
        </div>
        <div className={'audio-dialog-actions'}>
          <ErrorDialog.Close render={<Button />} className={'audio-dialog-close'} />
        </div>
      </ErrorDialog.Popup>
    </ErrorDialog.Root>
  );
}

type AudioPlayButtonProps = Omit<ComponentProps<'div'>, 'children'>;

type BufferingIndicatorProps = Omit<BufferingIndicatorPrimitive.Props, 'children'>;

function BufferingIndicator({ className, ...props }: BufferingIndicatorProps = {}) {
  return (
    <BufferingIndicatorPrimitive
      className={(state) => `media-buffering-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <SpinnerIconPrimitive className={'media-buffering-indicator-spinner-icon'} />
    </BufferingIndicatorPrimitive>
  );
}

interface ButtonTooltipProps extends Tooltip.RootProps {
  children: ReactElement;
  label?: ReactNode;
}

function ButtonTooltip({ children, label, ...props }: ButtonTooltipProps) {
  return (
    <Tooltip.Root {...props}>
      <Tooltip.Trigger render={children} />
      <Tooltip.Popup
        className={'media-popup media-popup-safe-area media-popup-transition media-popup-surface media-tooltip'}
      >
        {label ?? <Tooltip.Label />}
        {!label && <Tooltip.Shortcut className={'media-tooltip-shortcut'} />}
      </Tooltip.Popup>
    </Tooltip.Root>
  );
}

type PlayButtonProps = Omit<PlayButtonPrimitive.Props, 'children'>;

function PlayButton({ className, ...props }: PlayButtonProps = {}) {
  return (
    <PlayButtonPrimitive
      render={<Button />}
      className={(state) => `media-play-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <RestartIconPrimitive className={'media-button-icon media-play-button-restart-icon'} />
      <PlayIconPrimitive className={'media-button-icon media-play-button-play-icon'} />
      <PauseIconPrimitive className={'media-button-icon media-play-button-pause-icon'} />
    </PlayButtonPrimitive>
  );
}

function AudioPlayButton({ className, ...props }: AudioPlayButtonProps = {}) {
  return (
    <div className={`audio-play-button ${className ?? ''}`} {...props}>
      <BufferingIndicator className={'audio-play-button-buffering-indicator'} />
      <ButtonTooltip boundary="viewport" side="top">
        <PlayButton />
      </ButtonTooltip>
    </div>
  );
}

type LiveButtonProps = Omit<LiveButtonPrimitive.Props, 'children'>;

function LiveButton({ className, ...props }: LiveButtonProps = {}) {
  return (
    <LiveButtonPrimitive
      render={<Button />}
      className={(state) => `media-live-button ${resolveClassName(className, state)}`}
      {...props}
    />
  );
}

interface VolumePopoverProps extends Omit<VolumePopoverPrimitive.RootProps, 'children'> {
  className?: ClassValue;
  orientation?: CoreVolumeSliderProps['orientation'];
  showTooltip?: boolean;
}

type MuteButtonProps = Omit<MuteButtonPrimitive.Props, 'children'>;

function MuteButton({ className, ...props }: MuteButtonProps = {}) {
  return (
    <MuteButtonPrimitive
      render={<Button />}
      className={(state) => `media-mute-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <VolumeOffIconPrimitive className={'media-button-icon media-mute-button-off-icon'} />
      <VolumeLowIconPrimitive className={'media-button-icon media-mute-button-low-icon'} />
      <VolumeHighIconPrimitive className={'media-button-icon media-mute-button-high-icon'} />
    </MuteButtonPrimitive>
  );
}

type VolumeSliderProps = Omit<VolumeSliderPrimitive.RootProps, 'children'>;

function VolumeSlider({ className, ...props }: VolumeSliderProps = {}) {
  return (
    <VolumeSliderPrimitive.Root
      className={(state) => `media-slider media-volume-slider ${resolveClassName(className, state)}`}
      thumbAlignment="edge"
      {...props}
    >
      <VolumeSliderPrimitive.Track render={<SliderTrack />}>
        <VolumeSliderPrimitive.Fill render={<SliderFill />} />
      </VolumeSliderPrimitive.Track>
      <VolumeSliderPrimitive.Thumb render={<SliderThumb />} className={'media-volume-slider-thumb'} />
    </VolumeSliderPrimitive.Root>
  );
}

function MinimalLiveAudioControls() {
  return (
    <Controls.Root visibility="always">
      <Controls.Content className={'audio-controls audio-controls-content'}>
        <Tooltip.Provider>
          <Controls.Group className={'audio-controls-start'}>
            <AudioPlayButton />
            <LiveButton />
          </Controls.Group>

          <div aria-hidden="true" className={'audio-controls-spacer'} />

          <Controls.Group className={'audio-controls-end'}>
            <VolumePopover boundary="viewport" showTooltip side="left" orientation="horizontal" />
          </Controls.Group>
        </Tooltip.Provider>
      </Controls.Content>
    </Controls.Root>
  );
}

interface LivePlaybackHotkeysProps {
  disabled?: boolean | undefined;
}

function LivePlaybackHotkeys({ disabled = false }: LivePlaybackHotkeysProps = {}) {
  return (
    <>
      <Hotkey disabled={disabled} keys="Space" action="togglePaused" />
      <Hotkey disabled={disabled} keys="k" action="togglePaused" />
      <Hotkey disabled={disabled} keys="m" action="toggleMuted" />
      <Hotkey disabled={disabled} keys="ArrowUp" action="volumeStep" />
      <Hotkey disabled={disabled} keys="ArrowDown" action="volumeStep" />
    </>
  );
}

type StatusAnnouncerProps = Omit<StatusAnnouncerPrimitive.Props, 'children'>;

function StatusAnnouncer({ className, ...props }: StatusAnnouncerProps = {}) {
  return (
    <StatusAnnouncerPrimitive
      className={(state) => `media-status-announcer ${resolveClassName(className, state)}`}
      {...props}
    />
  );
}

function MinimalLiveAudioSkin({ children, className, ...props }: MinimalLiveAudioSkinProps = {}) {
  return (
    <Container className={`audio-skin ${className ?? ''}`} data-theme="minimal" data-preset="live-audio" {...props}>
      {children}
      <AudioErrorDialog />
      <MinimalLiveAudioControls />
      <LivePlaybackHotkeys />
      <StatusAnnouncer />
    </Container>
  );
}

const Skin = MinimalLiveAudioSkin;

// ================================================================
// Player
// ================================================================

export interface LiveAudioPlayerProps {
  children?: ReactNode;
  style?: CSSProperties;
  className?: string;
}

export function MinimalLiveAudioSkin(props: MinimalLiveAudioSkinProps) {
  return <Skin {...props} />;
}

// ================================================================
// Components
// ================================================================

function Button({ className, ...props }: ButtonProps) {
  return <button className={`media-button ${className ?? ''}`} {...props} />;
}

type SliderTrackProps = ComponentProps<'div'>;

function SliderTrack({ className, ...props }: SliderTrackProps) {
  return <div className={`media-slider-track ${className ?? ''}`} {...props} />;
}

type SliderFillProps = ComponentProps<'div'>;

function SliderFill({ className, ...props }: SliderFillProps) {
  return <div className={`media-slider-fill ${className ?? ''}`} {...props} />;
}

type SliderThumbProps = ComponentProps<'div'>;

function SliderThumb({ className, ...props }: SliderThumbProps) {
  return <div className={`media-slider-thumb ${className ?? ''}`} {...props} />;
}

function VolumePopover({
  className,
  showTooltip = false,
  side = 'top',
  orientation = 'vertical',
  ...props
}: VolumePopoverProps = {}) {
  return (
    <VolumePopoverPrimitive.Root openOnHover delay={200} closeDelay={100} side={side} {...props}>
      <ButtonTooltip delay={0} disabled={!showTooltip} sticky side="top">
        <VolumePopoverPrimitive.Trigger render={<MuteButton className={`${className ?? ''}`} />} />
      </ButtonTooltip>
      <VolumePopoverPrimitive.Popup
        className={'media-popup media-popup-safe-area media-popup-transition media-popup-surface media-volume-popover'}
      >
        <VolumeSlider orientation={orientation} />
      </VolumePopoverPrimitive.Popup>
    </VolumePopoverPrimitive.Root>
  );
}