npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fairu/player

v1.2.0

Published

A lightweight, modular React podcast player with TypeScript

Downloads

362

Readme

@fairu/player

A lightweight, modular React media player with TypeScript support. Supports audio podcasts and video with HLS streaming. Designed to be embeddable as a widget for fairu.app and external sites.

Features

  • React 18+ with TypeScript - Full type safety and modern React features
  • Audio & Video Player - Unified API for audio podcasts and video content
  • HLS Streaming - Adaptive bitrate streaming with quality selection
  • Live Streaming - HLS live streams with low latency mode
  • Tailwind CSS + CSS Variables - Easy theming with CSS custom properties
  • Playlist Support - Queue management, shuffle, repeat modes
  • Chapters - Display and navigate podcast chapters
  • Subtitles - Video subtitle/caption support
  • Fullscreen Mode - Native fullscreen with keyboard controls
  • Watch Progress Tracking - Track watched segments and completion
  • Embeddable - Script-based and iframe embedding options
  • GDPR Compliant - Opt-in tracking with configurable endpoints
  • Ads Support - Pre-roll, mid-roll, and post-roll ad integration with VAST tracking
  • Video Ads - Video ads and custom component ads
  • Keyboard Controls - Full keyboard navigation support
  • Accessible - ARIA labels and focus management
  • Hooks API - Composable hooks for custom player implementations

Installation

npm install @fairu/player

Quick Start

Basic Audio Usage

import { PlayerProvider, Player } from '@fairu/player';
import '@fairu/player/styles.css';

function App() {
  return (
    <PlayerProvider
      config={{
        track: {
          id: '1',
          src: 'https://example.com/podcast.mp3',
          title: 'My Podcast Episode',
          artist: 'Podcast Host',
          artwork: 'https://example.com/artwork.jpg',
        },
      }}
    >
      <Player />
    </PlayerProvider>
  );
}

Basic Video Usage

import { VideoProvider, VideoPlayer } from '@fairu/player';
import '@fairu/player/styles.css';

function App() {
  return (
    <VideoProvider
      config={{
        track: {
          id: '1',
          src: 'https://example.com/video.mp4',
          title: 'My Video',
          poster: 'https://example.com/poster.jpg',
        },
      }}
    >
      <VideoPlayer />
    </VideoProvider>
  );
}

Video with HLS Streaming

import { VideoProvider, VideoPlayer } from '@fairu/player';

const videoTrack = {
  id: '1',
  src: 'https://example.com/video.m3u8',
  title: 'HLS Video',
  poster: 'https://example.com/poster.jpg',
};

function App() {
  return (
    <VideoProvider
      config={{
        track: videoTrack,
        hls: {
          enabled: true,
          autoQuality: true,
          startLevel: -1, // Auto-select starting quality
          maxBufferLength: 30,
          lowLatencyMode: false,
        },
      }}
    >
      <VideoPlayer />
    </VideoProvider>
  );
}

Live Streaming

The player supports live streaming via HLS. For optimal live stream playback, enable lowLatencyMode:

import { VideoProvider, VideoPlayer } from '@fairu/player';

function LiveStream() {
  return (
    <VideoProvider
      config={{
        track: {
          id: 'live-1',
          src: 'https://stream.example.com/live.m3u8',
          title: 'Live Broadcast',
        },
        hls: {
          enabled: true,
          lowLatencyMode: true, // Reduces latency for live streams
          maxBufferLength: 10,  // Smaller buffer for live
        },
      }}
    >
      <VideoPlayer />
    </VideoProvider>
  );
}

Supported Live Stream Formats:

| Format | Support | Notes | |--------|---------|-------| | HLS Live (.m3u8) | Yes | Full support with hls.js | | Audio Streams (Icecast/Shoutcast) | Yes | Via native HTML5 audio | | DASH Live | No | Not currently supported | | WebRTC | No | Not currently supported |

Current Limitations:

  • No built-in "LIVE" badge indicator
  • Progress bar shows buffered position (not live edge indicator)
  • No "Go to Live" button for DVR streams
  • Duration displays as stream length, not "LIVE"

For full live streaming UI features (LIVE badge, DVR controls, live edge indicator), a future update is planned. The current implementation focuses on reliable playback of HLS live streams with low latency support.

Playlist

import { PlayerProvider, Player } from '@fairu/player';
import '@fairu/player/styles.css';

function App() {
  return (
    <PlayerProvider
      config={{
        playlist: [
          { id: '1', src: 'episode1.mp3', title: 'Episode 1' },
          { id: '2', src: 'episode2.mp3', title: 'Episode 2' },
          { id: '3', src: 'episode3.mp3', title: 'Episode 3' },
        ],
        shuffle: false,
        repeat: 'all',
      }}
    >
      <Player showPlaylist />
    </PlayerProvider>
  );
}

With Chapters

const track = {
  id: '1',
  src: 'podcast.mp3',
  title: 'Podcast Episode',
  chapters: [
    { id: 'ch1', title: 'Introduction', startTime: 0 },
    { id: 'ch2', title: 'Main Topic', startTime: 120 },
    { id: 'ch3', title: 'Conclusion', startTime: 300 },
  ],
};

<PlayerProvider config={{ track }}>
  <Player showChapters />
</PlayerProvider>

Video with Subtitles

const videoTrack = {
  id: '1',
  src: 'https://example.com/video.mp4',
  title: 'Video with Subtitles',
  poster: 'https://example.com/poster.jpg',
  subtitles: [
    { id: 'en', label: 'English', language: 'en', src: '/subtitles/en.vtt', default: true },
    { id: 'de', label: 'Deutsch', language: 'de', src: '/subtitles/de.vtt' },
  ],
};

<VideoProvider config={{ track: videoTrack, features: { subtitles: true } }}>
  <VideoPlayer />
</VideoProvider>

Components

Audio Components

| Component | Description | |-----------|-------------| | Player | Complete audio player with all controls | | PlayButton | Play/pause toggle button | | ProgressBar | Seek bar with buffering indicator | | TimeDisplay | Current time / duration display | | VolumeControl | Volume slider with mute button | | PlaybackSpeed | Playback rate selector | | SkipButtons | Forward/backward skip buttons | | PlaylistView | Playlist panel with track list | | TrackItem | Individual track in playlist | | PlaylistControls | Shuffle/repeat controls | | ChapterList | Chapter navigation list | | ChapterMarker | Chapter marker on progress bar |

Video Components

| Component | Description | |-----------|-------------| | VideoPlayer | Complete video player with all controls | | VideoOverlay | Overlay for play button and loading states | | VideoControls | Bottom control bar for video | | FullscreenButton | Fullscreen toggle button | | QualitySelector | HLS quality level selector |

Ad Components

| Component | Description | |-----------|-------------| | AdOverlay | Ad display overlay | | AdSkipButton | Skip ad button with countdown |

Hooks API

The player provides composable hooks for building custom player UIs:

usePlayer

Access the audio player context for state and controls.

import { usePlayer } from '@fairu/player';

function CustomControls() {
  const { state, controls } = usePlayer();

  return (
    <div>
      <button onClick={controls.togglePlay}>
        {state.isPlaying ? 'Pause' : 'Play'}
      </button>
      <span>{state.currentTime} / {state.duration}</span>
    </div>
  );
}

useVideoPlayer

Access the video player context for video-specific features.

import { useVideoPlayer } from '@fairu/player';

function CustomVideoControls() {
  const { state, controls, currentTrack } = useVideoPlayer();

  return (
    <div>
      <button onClick={controls.togglePlay}>
        {state.isPlaying ? 'Pause' : 'Play'}
      </button>
      <button onClick={controls.toggleFullscreen}>
        {state.isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
      </button>
      <span>Quality: {state.currentQuality}</span>
    </div>
  );
}

useMedia

Generic media hook that works with any HTMLMediaElement.

import { useMedia } from '@fairu/player';

function CustomAudioPlayer() {
  const audioRef = useRef<HTMLAudioElement>(null);
  const { state, controls } = useMedia(audioRef, { src: 'audio.mp3' });

  return (
    <>
      <audio ref={audioRef} />
      <button onClick={controls.togglePlay}>
        {state.isPlaying ? 'Pause' : 'Play'}
      </button>
    </>
  );
}

useHLS

HLS-specific functionality for adaptive streaming.

import { useHLS, isHLSSource } from '@fairu/player';

function HLSPlayer() {
  const videoRef = useRef<HTMLVideoElement>(null);
  const {
    isReady,
    availableQualities,
    currentQuality,
    setQuality,
    isAutoQuality,
    setAutoQuality
  } = useHLS(videoRef, {
    src: 'https://example.com/video.m3u8',
    autoQuality: true,
  });

  return (
    <>
      <video ref={videoRef} />
      <select
        value={currentQuality}
        onChange={(e) => setQuality(e.target.value)}
      >
        <option value="auto">Auto</option>
        {availableQualities.map(q => (
          <option key={q.label} value={q.label}>{q.label}</option>
        ))}
      </select>
    </>
  );
}

useFullscreen

Fullscreen management for any container element.

import { useFullscreen } from '@fairu/player';

function FullscreenContainer() {
  const containerRef = useRef<HTMLDivElement>(null);
  const { isFullscreen, enterFullscreen, exitFullscreen, toggleFullscreen } = useFullscreen(containerRef);

  return (
    <div ref={containerRef}>
      <button onClick={toggleFullscreen}>
        {isFullscreen ? 'Exit' : 'Enter'} Fullscreen
      </button>
    </div>
  );
}

usePlaylist

Playlist management with shuffle and repeat.

import { usePlaylist } from '@fairu/player';

function PlaylistManager() {
  const {
    tracks,
    currentIndex,
    currentTrack,
    shuffle,
    repeat,
    hasNext,
    hasPrevious,
    playTrack,
    next,
    previous,
    toggleShuffle,
    setRepeat,
  } = usePlaylist({
    tracks: [...],
    initialIndex: 0,
  });

  return (
    <div>
      {tracks.map((track, i) => (
        <div key={track.id} onClick={() => playTrack(i)}>
          {currentIndex === i && '▶'} {track.title}
        </div>
      ))}
    </div>
  );
}

useChapters

Chapter navigation for podcasts.

import { useChapters } from '@fairu/player';

function ChapterNav() {
  const { chapters, currentChapter, goToChapter } = useChapters();

  return (
    <ul>
      {chapters.map(chapter => (
        <li
          key={chapter.id}
          onClick={() => goToChapter(chapter)}
          className={currentChapter?.id === chapter.id ? 'active' : ''}
        >
          {chapter.title}
        </li>
      ))}
    </ul>
  );
}

useKeyboardControls

Enable keyboard shortcuts for player controls.

import { useKeyboardControls } from '@fairu/player';

function PlayerWithKeyboard() {
  useKeyboardControls({
    enabled: true,
    scope: 'global', // or 'focused'
  });

  return <Player />;
}

useAds / useVideoAds

Access ad state and controls.

import { useAds, useVideoAds } from '@fairu/player';

function AdIndicator() {
  const { state } = useAds(); // or useVideoAds() for video

  if (!state.isPlayingAd) return null;

  return (
    <div>
      Ad {state.adsRemaining} remaining
      {state.canSkip && <button>Skip</button>}
    </div>
  );
}

Configuration

PlayerConfig (Audio)

| Option | Type | Default | Description | |--------|------|---------|-------------| | track | Track | - | Single track to play | | playlist | Track[] | - | Array of tracks for playlist mode | | features | PlayerFeatures | all enabled | Enable/disable player features | | autoPlayNext | boolean | true | Auto-play next track in playlist | | shuffle | boolean | false | Enable shuffle mode | | repeat | 'none' \| 'one' \| 'all' | 'none' | Repeat mode | | skipForwardSeconds | number | 30 | Skip forward duration | | skipBackwardSeconds | number | 10 | Skip backward duration | | playbackSpeeds | number[] | [0.5, 0.75, 1, 1.25, 1.5, 2] | Available playback speeds | | volume | number | 1 | Initial volume (0-1) | | muted | boolean | false | Start muted | | autoPlay | boolean | false | Auto-play on load |

VideoConfig

| Option | Type | Default | Description | |--------|------|---------|-------------| | track | VideoTrack | - | Single video track | | playlist | VideoTrack[] | - | Video playlist | | features | VideoFeatures | all enabled | Enable/disable features | | poster | string | - | Default poster image | | controlsHideDelay | number | 3000 | Auto-hide controls delay (ms) | | hls | HLSConfig | - | HLS streaming configuration | | autoPlayNext | boolean | true | Auto-play next video | | shuffle | boolean | false | Shuffle mode | | repeat | RepeatMode | 'none' | Repeat mode |

HLSConfig

| Option | Type | Default | Description | |--------|------|---------|-------------| | enabled | boolean | true | Enable HLS support | | autoQuality | boolean | true | Auto-select quality based on bandwidth | | startLevel | number | -1 | Starting quality (-1 for auto) | | maxBufferLength | number | 30 | Max buffer length in seconds | | lowLatencyMode | boolean | false | Enable low latency for live streams |

VideoFeatures

interface VideoFeatures {
  chapters?: boolean;        // Show chapter markers
  volumeControl?: boolean;   // Show volume slider
  playbackSpeed?: boolean;   // Show speed selector
  skipButtons?: boolean;     // Show skip buttons
  progressBar?: boolean;     // Show progress bar
  timeDisplay?: boolean;     // Show time display
  playlistView?: boolean;    // Show playlist panel
  fullscreen?: boolean;      // Show fullscreen button
  qualitySelector?: boolean; // Show quality selector (HLS)
  subtitles?: boolean;       // Enable subtitles
  pictureInPicture?: boolean; // Enable PiP mode
  autoHideControls?: boolean; // Auto-hide controls
  seekingDisabled?: boolean;  // Disable seeking
}

Ads

Audio Ads

import { AdProvider, PlayerProvider, Player } from '@fairu/player';

<AdProvider
  config={{
    enabled: true,
    adBreaks: [
      {
        id: 'pre1',
        position: 'pre-roll',
        ads: [
          {
            id: 'ad1',
            src: 'https://example.com/ad.mp3',
            duration: 15,
            skipAfterSeconds: 5,
            clickThroughUrl: 'https://example.com',
          },
        ],
      },
      {
        id: 'mid1',
        position: 'mid-roll',
        triggerTime: 300, // 5 minutes
        ads: [{ id: 'ad2', src: 'ad2.mp3', duration: 30 }],
      },
    ],
    onAdStart: (ad, adBreak) => console.log('Ad started', ad.id),
    onAdComplete: (ad, adBreak) => console.log('Ad complete', ad.id),
  }}
>
  <PlayerProvider config={playerConfig}>
    <Player />
  </PlayerProvider>
</AdProvider>

Video Ads

import { VideoAdProvider, VideoProvider, VideoPlayer } from '@fairu/player';

<VideoAdProvider
  config={{
    enabled: true,
    adBreaks: [
      {
        id: 'pre1',
        position: 'pre-roll',
        ads: [
          {
            id: 'ad1',
            src: 'https://example.com/ad.mp4',
            duration: 15,
            skipAfterSeconds: 5,
            poster: 'https://example.com/ad-poster.jpg',
            clickThroughUrl: 'https://example.com',
            trackingUrls: {
              impression: 'https://tracking.example.com/impression',
              start: 'https://tracking.example.com/start',
              complete: 'https://tracking.example.com/complete',
            },
          },
        ],
      },
    ],
  }}
>
  <VideoProvider config={videoConfig}>
    <VideoPlayer />
  </VideoProvider>
</VideoAdProvider>

Custom Component Ads

You can render custom React components instead of video ads:

const CustomAdComponent = ({ onComplete, onSkip, canSkip, skipCountdown, ad }) => (
  <div className="custom-ad">
    <h2>{ad.title}</h2>
    <img src={ad.poster} alt={ad.title} />
    <button onClick={onComplete}>Continue</button>
    {canSkip && <button onClick={onSkip}>Skip Ad</button>}
    {!canSkip && <span>Skip in {skipCountdown}s</span>}
  </div>
);

const adBreaks = [
  {
    id: 'custom1',
    position: 'mid-roll',
    triggerTime: 60,
    ads: [
      {
        id: 'ad1',
        src: '', // Not used for component ads
        duration: 10,
        component: CustomAdComponent,
      },
    ],
  },
];

Dynamic Ad Triggering (Event Pipeline)

For advanced use cases, you can trigger overlay ads and info cards programmatically from anywhere in your application using the AdEventBus. This is useful for:

  • Analytics-driven ad triggers (show ads when users reach milestones)
  • E-commerce integration (cart abandonment reminders)
  • Real-time events (WebSocket-triggered promotions)
  • Timer-based campaigns
  • A/B testing different ad placements
import { createAdEventBus, VideoPlayer } from '@fairu/player';

// Create an event bus (can be a singleton for app-wide access)
const adEventBus = createAdEventBus();

function App() {
  return (
    <VideoPlayer
      track={myTrack}
      adEventBus={adEventBus}  // Pass the event bus to the player
    />
  );
}

// Trigger ads from ANYWHERE in your app:

// Show an overlay ad
adEventBus.emit('showOverlayAd', {
  id: 'promo-1',
  imageUrl: 'https://example.com/promo.png',
  clickThroughUrl: 'https://example.com/offer',
  displayAt: 0,  // Ignored for manual triggers
  closeable: true,
  position: 'bottom',
});

// Hide a specific overlay ad
adEventBus.emit('hideOverlayAd', { id: 'promo-1' });

// Hide all overlay ads
adEventBus.emit('hideAllOverlayAds');

// Show an info card
adEventBus.emit('showInfoCard', {
  id: 'product-1',
  type: 'product',
  title: 'Featured Product',
  description: 'Check out this deal!',
  price: '$49.99',
  url: 'https://example.com/product',
  displayAt: 0,
  position: 'top-right',
});

// Hide info cards
adEventBus.emit('hideInfoCard', { id: 'product-1' });
adEventBus.emit('hideAllInfoCards');

// Reset all dismissed ads (allow them to show again)
adEventBus.emit('resetDismissed');

Available Events

| Event | Payload | Description | |-------|---------|-------------| | showOverlayAd | OverlayAd | Show an overlay banner ad | | hideOverlayAd | { id: string } | Hide a specific overlay ad | | hideAllOverlayAds | - | Hide all overlay ads | | showInfoCard | InfoCard | Show an info card | | hideInfoCard | { id: string } | Hide a specific info card | | hideAllInfoCards | - | Hide all info cards | | resetDismissed | - | Reset dismissed states |

Integration Examples

// Analytics integration
analytics.on('userMilestone', (milestone) => {
  if (milestone === '50%_watched') {
    adEventBus.emit('showOverlayAd', promoAd);
  }
});

// E-commerce cart abandonment
cart.on('abandoned', () => {
  adEventBus.emit('showInfoCard', {
    id: 'cart-reminder',
    type: 'product',
    title: 'Complete your purchase!',
    price: '-20% with code SAVE20',
    displayAt: 0,
  });
});

// WebSocket real-time events
socket.on('flash-sale', (saleData) => {
  adEventBus.emit('showOverlayAd', {
    id: 'flash-sale',
    imageUrl: saleData.bannerUrl,
    clickThroughUrl: saleData.url,
    displayAt: 0,
    closeable: true,
  });
});

Global Event Bus

For app-wide access, you can use a singleton pattern:

import { getGlobalAdEventBus, resetGlobalAdEventBus } from '@fairu/player';

// Get the global instance (created on first access)
const adEventBus = getGlobalAdEventBus();

// Use it anywhere in your app
adEventBus.emit('showOverlayAd', myAd);

// Reset on unmount or cleanup
resetGlobalAdEventBus();

VAST Tracking Events

The player supports standard VAST tracking events:

| Event | Description | |-------|-------------| | impression | Ad is displayed | | start | Playback begins | | firstQuartile | 25% watched | | midpoint | 50% watched | | thirdQuartile | 75% watched | | complete | 100% watched | | skip | User skipped the ad | | click | User clicked the ad | | error | Playback error occurred | | pause | Ad paused | | resume | Ad resumed | | mute | Audio muted | | unmute | Audio unmuted |

TypeScript Types

Core Types

import type {
  // Track types
  Track,
  VideoTrack,
  Chapter,
  Subtitle,
  VideoQuality,

  // State types
  PlayerState,
  VideoState,
  PlaylistState,
  WatchProgress,

  // Config types
  PlayerConfig,
  VideoConfig,
  HLSConfig,
  PlayerFeatures,
  VideoFeatures,

  // Ad types
  Ad,
  AdBreak,
  AdPosition,
  VideoAd,
  VideoAdBreak,
  AdConfig,
  VideoAdConfig,
  AdTrackingUrls,

  // Control types
  PlayerControls,
  VideoControls,
  PlaylistControls,

  // Context types
  PlayerContextValue,
  VideoContextValue,
} from '@fairu/player';

Track Type

interface Track {
  id: string;
  src: string;
  title: string;
  artist?: string;
  album?: string;
  artwork?: string;
  duration?: number;
  chapters?: Chapter[];
}

interface VideoTrack extends Track {
  type?: 'video';
  poster?: string;
  qualities?: VideoQuality[];
  subtitles?: Subtitle[];
}

WatchProgress Type

interface WatchProgress {
  watchedSegments: WatchedSegment[];
  percentageWatched: number;
  isFullyWatched: boolean;
  furthestPoint: number;
}

interface WatchedSegment {
  start: number;
  end: number;
}

Embedding

Script-based Embed

<div
  data-fairu-player
  data-src="https://example.com/podcast.mp3"
  data-title="My Podcast"
  data-theme="dark"
></div>
<script src="https://fairu.app/player/embed.js" data-auto-init></script>

Programmatic Embed

<div id="my-player"></div>
<script src="https://fairu.app/player/embed.js"></script>
<script>
  FairuPlayer.create('#my-player', {
    player: {
      track: {
        id: '1',
        src: 'https://example.com/podcast.mp3',
        title: 'My Podcast',
      },
    },
    theme: 'light',
  });
</script>

Iframe Embed

<iframe
  src="https://fairu.app/embed/player?src=https://example.com/podcast.mp3&theme=dark"
  width="100%"
  height="150"
  frameborder="0"
></iframe>

CDN Usage

The player is available as a standalone script for direct inclusion via CDN. Two variants are provided:

Standalone (recommended for most users)

Includes React bundled - no dependencies required (~540 KB):

<link rel="stylesheet" href="https://unpkg.com/@fairu/player/dist/player.css">
<script src="https://unpkg.com/@fairu/player/dist/fairu-player.iife.js"></script>

<div data-fairu-player data-src="https://example.com/audio.mp3"></div>

<script>
  FairuPlayer.init();
</script>

Lightweight (for sites already using React)

Requires external React 18+ (~66 KB):

<link rel="stylesheet" href="https://unpkg.com/@fairu/player/dist/player.css">
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@fairu/player/dist/fairu-player.light.iife.js"></script>

<div data-fairu-player data-src="https://example.com/audio.mp3"></div>

<script>
  FairuPlayer.init();
</script>

CDN API

// Initialize all elements with data-fairu-player attribute
FairuPlayer.init();

// Initialize with custom selector
FairuPlayer.init('.my-player');

// Mount programmatically
FairuPlayer.create('#my-container', {
  src: 'https://example.com/audio.mp3',
  player: {
    showWaveform: false,
  }
});

// Unmount
FairuPlayer.unmount(element);

Data Attributes

Configure the player using data attributes:

| Attribute | Description | |-----------|-------------| | data-src | Media URL | | data-title | Track title | | data-artist | Artist name | | data-artwork | Artwork URL | | data-theme | Theme (light or dark) |

Theming

The player uses CSS custom properties for theming:

:root {
  --fp-color-primary: #6366f1;
  --fp-color-background: #ffffff;
  --fp-color-surface: #f3f4f6;
  --fp-color-text: #1f2937;
  --fp-color-text-muted: #6b7280;
  --fp-progress-bg: #e5e7eb;
  --fp-progress-fill: var(--fp-color-primary);
  --fp-border-radius: 8px;
}

[data-theme="dark"] {
  --fp-color-background: #1f2937;
  --fp-color-surface: #374151;
  --fp-color-text: #f9fafb;
}

Tracking

Enable GDPR-compliant tracking:

import { PlayerProvider, TrackingProvider, Player } from '@fairu/player';

<TrackingProvider
  config={{
    enabled: true, // Must be explicitly enabled (GDPR)
    endpoint: 'https://api.example.com/track',
    events: {
      play: true,
      pause: true,
      progress: true,
      complete: true,
    },
    progressIntervals: [25, 50, 75, 100],
  }}
>
  <PlayerProvider config={playerConfig}>
    <Player />
  </PlayerProvider>
</TrackingProvider>

Keyboard Shortcuts

| Key | Action | |-----|--------| | Space / K | Play/Pause | | | Skip backward 5s | | | Skip forward 5s | | Shift + ← | Skip backward 10s | | Shift + → | Skip forward 10s | | | Volume up | | | Volume down | | M | Toggle mute | | J | Skip backward 10s | | L | Skip forward 10s | | 0-9 | Seek to 0-90% | | Home | Go to start | | End | Go to end | | F | Toggle fullscreen (video) | | C | Toggle subtitles (video) |

Storybook

The project includes a comprehensive Storybook setup for interactive component development and documentation.

Running Storybook

npm run storybook

This starts Storybook at http://localhost:6006.

Available Stories

| Category | Components | |----------|------------| | Player | Player, NowPlayingView, CoverArtView | | VideoPlayer | VideoPlayer, VideoOverlay, VideoControls, DynamicAdTriggering, EventPipeline | | Controls | PlayButton, ProgressBar, VolumeControl, TimeDisplay, PlaybackSpeed, SkipButtons, FullscreenButton, QualitySelector | | Playlist | PlaylistView, TrackItem, PlaylistControls | | Chapters | ChapterList, ChapterMarker | | Ads | AdOverlay, AdSkipButton, OverlayAd, InfoCard |

Story Features

Each component includes multiple story variants:

  • Default - Basic usage with minimal props
  • With Controls - Interactive Storybook controls for all props
  • Edge Cases - Long text, missing data, loading states
  • Theming - Light/dark mode variants

Building Static Storybook

npm run build-storybook

This generates a static build in the storybook-static directory, ready for deployment.

Development

# Install dependencies
npm install

# Start development server
npm run dev

# Run Storybook
npm run storybook

# Run tests
npm run test

# Build library
npm run build:lib

# Type checking
npm run typecheck

Fairu.app Hosting

The player includes built-in support for fairu.app as a media hosting solution. When using fairu.app, you only need to provide the file UUID - the player automatically constructs the correct URLs for media files and cover images.

Quick Start with Fairu Hosting

import { PlayerProvider, Player, createTrackFromFairu } from '@fairu/player';
import '@fairu/player/styles.css';

function App() {
  // Just provide the UUID from fairu.app
  const track = createTrackFromFairu({
    uuid: '123e4567-e89b-12d3-a456-426614174000',
    title: 'My Podcast Episode',
    artist: 'Podcast Host',
  });

  return (
    <PlayerProvider config={{ track }}>
      <Player />
    </PlayerProvider>
  );
}

Video with Fairu Hosting

import { VideoProvider, VideoPlayer, createVideoTrackFromFairu } from '@fairu/player';

function App() {
  const track = createVideoTrackFromFairu({
    uuid: '123e4567-e89b-12d3-a456-426614174000',
    title: 'My Video',
    version: 'high', // Optional: 'low', 'medium', or 'high'
  });

  return (
    <VideoProvider config={{ track }}>
      <VideoPlayer />
    </VideoProvider>
  );
}

Playlist with Fairu Hosting

import { createPlaylistFromFairu, createVideoPlaylistFromFairu } from '@fairu/player';

// Audio playlist
const audioPlaylist = createPlaylistFromFairu([
  { uuid: 'uuid-1', title: 'Episode 1' },
  { uuid: 'uuid-2', title: 'Episode 2' },
  { uuid: 'uuid-3', title: 'Episode 3' },
]);

// Video playlist
const videoPlaylist = createVideoPlaylistFromFairu([
  { uuid: 'uuid-1', title: 'Video 1', version: 'high' },
  { uuid: 'uuid-2', title: 'Video 2', version: 'high' },
]);

Fairu URL Utilities

The package exports utility functions for generating fairu.app URLs:

import {
  getFairuAudioUrl,
  getFairuVideoUrl,
  getFairuHlsUrl,
  getFairuCoverUrl,
  getFairuThumbnailUrl,
} from '@fairu/player';

// Audio URL
const audioUrl = getFairuAudioUrl('uuid');
// → https://files.fairu.app/uuid/audio.mp3

// Video URL with quality
const videoUrl = getFairuVideoUrl('uuid', { version: 'high' });
// → https://files.fairu.app/uuid/video.mp4?version=high

// HLS streaming URL
const hlsUrl = getFairuHlsUrl('uuid', 'tenant-id');
// → https://files.fairu.app/hls/tenant-id/uuid/master.m3u8

// Cover image with dimensions
const coverUrl = getFairuCoverUrl('uuid', { width: 800, height: 450 });
// → https://files.fairu.app/uuid/cover.jpg?width=800&height=450

// Video thumbnail at specific timestamp
const thumbUrl = getFairuThumbnailUrl('uuid', '00:01:30.000', { width: 320 });
// → https://files.fairu.app/uuid/thumbnail.jpg?timestamp=00:01:30.000&width=320

Fairu Track Types

import type { FairuTrack, FairuVideoTrack } from '@fairu/player';

// Audio track
const audioTrack: FairuTrack = {
  uuid: '123e4567-e89b-12d3-a456-426614174000',
  title: 'My Podcast',
  artist: 'Host Name',
  album: 'Podcast Series',
  duration: 3600,
  coverOptions: {
    width: 400,
    height: 400,
    format: 'webp',
  },
};

// Video track
const videoTrack: FairuVideoTrack = {
  uuid: '123e4567-e89b-12d3-a456-426614174000',
  title: 'My Video',
  version: 'high',
  posterOptions: {
    width: 1280,
    height: 720,
  },
};

Cover Image Options

When generating cover images, you can customize the output:

| Option | Type | Default | Description | |--------|------|---------|-------------| | width | number | 400 | Width in pixels (1-6000) | | height | number | 400 | Height in pixels (1-6000) | | format | 'jpg' \| 'png' \| 'webp' | - | Output format | | quality | number | 95 | Quality for JPEG/WebP (1-100) | | fit | 'cover' \| 'contain' | 'cover' | Resize mode | | focal | string | - | Focal point for smart crop ("x-y-zoom") |

License

Fairu Source Available License

This software is source available under a custom license. You can use, modify, and distribute this player freely, except for uses that compete with fairu.app's media hosting services.

What you CAN do:

  • Use the player to embed and play your own media content
  • Use it with any hosting service (self-hosted, CDN, etc.)
  • Modify and customize the player
  • Use it in commercial projects
  • Use it in open source projects

What you CANNOT do:

  • Build a competing media hosting/player service using this code

For alternative licensing arrangements, contact [email protected].

See LICENSE for the full license text.