Skip to content

sushidev-team/fairu-player

Repository files navigation

@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 support@sushi.dev.

See LICENSE for the full license text.

About

Audio/Video Player for Fairu.app

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •