Skip to main content

PersistentPlayer Component

The PersistentPlayer is a persistent audio player component that remains fixed on screen and continues playing even when navigating between pages. It features automatic playback time saving, volume persistence, and a sleep timer.

Location

src/components/PersistentPlayer/PersistentPlayer.jsx

Import

import PersistentPlayer from "../PersistentPlayer/PersistentPlayer";

Props

onClose
function
required
Callback function triggered when the close button is clicked
() => void

Features

Persistent Audio Playback

Uses react-h5-audio-player for robust HTML5 audio handling:
import AudioPlayer from "react-h5-audio-player";
import "react-h5-audio-player/lib/styles.css";

<AudioPlayer
  ref={audioRef}
  src={currentPodcast.audio}
  showJumpControls={true}
  layout="stacked-reverse"
  customProgressBarSection={["CURRENT_TIME", "PROGRESS_BAR", "DURATION"]}
  onPlay={handlePlay}
  onPause={() => dispatch(togglePlay(false))}
  onListen={handleTimeUpdate}
  onEnded={handleEnded}
  volume={volume}
  onVolumeChange={handleVolumeChange}
  showLoopControl={true}
/>

Playback Time Tracking

Automatically saves playback progress every second:
const handleTimeUpdate = (e) => {
  const currentTime = e.target.currentTime;
  const now = Date.now();

  // Update Redux state every 1 second (throttled)
  if (now - lastTimeUpdateRef.current >= 1000) {
    dispatch(updatePlaybackTime({ 
      title: currentPodcast.title, 
      time: currentTime 
    }));
    lastTimeUpdateRef.current = now;
  }
};
Playback time is throttled to update once per second to avoid excessive Redux dispatches.

Resume Playback

Automatically resumes from saved position:
useEffect(() => {
  if (audioRef.current && currentPodcast) {
    const savedTime = savePlaybackTime 
      ? playbackTimes[currentPodcast.title] || 0 
      : 0;
    const audio = audioRef.current.audio.current;

    audio.currentTime = savedTime;
    audio.volume = volume;

    if (isPlaying) {
      audio.play();
    } else {
      audio.pause();
    }
  }
}, [isPlaying, currentPodcast]);

Volume Persistence

Volume is saved to localStorage:
const [volume, setVolume] = useState(() => {
  const savedVolume = localStorage.getItem("nsnPlayerVolume");
  return savedVolume ? parseFloat(savedVolume) : 1;
});

const handleVolumeChange = (e) => {
  const newVolume = e.target.volume;
  setVolume(newVolume);
  localStorage.setItem("nsnPlayerVolume", newVolume.toString());
};

Auto-Complete on Episode End

When an episode finishes playing:
const handleEnded = () => {
  // Reset playback time to 0
  dispatch(updatePlaybackTime({ 
    title: currentPodcast.title, 
    time: 0 
  }));
  
  // Mark episode as completed
  dispatch(markAsCompleted(currentPodcast.title));
};

Remove from Completed on Play

If user replays a completed episode:
const handlePlay = () => {
  if (completedEpisodes.includes(currentPodcast.title)) {
    dispatch(removeFromCompleted(currentPodcast.title));
  }
  dispatch(togglePlay(true));
};

Redux State

Connects to multiple Redux slices:
const { 
  currentPodcast,  // Current podcast object
  isPlaying        // Playback state boolean
} = useSelector((state) => state.player);

Sleep Timer Integration

Includes integrated sleep timer component:
import SleepTimer from "../SleepTimer/SleepTimer";

<div className={styles.playerControls}>
  <AudioPlayer {...audioPlayerProps} />
  <SleepTimer />
</div>
See SleepTimer documentation for details on timer functionality.

Animations

Slides in from the right with spring animation:
<motion.div
  className={styles.persistentPlayer}
  initial={{ opacity: 0, x: windowWidth }}
  animate={{ opacity: 1, x: 0 }}
  exit={{ 
    opacity: 0, 
    x: windowWidth - 100, 
    transition: { duration: 0.9 } 
  }}
  transition={{ 
    duration: 0.9, 
    type: "spring", 
    stiffness: 120, 
    damping: 10 
  }}
>

Display Information

Shows current podcast metadata:
<div className={styles.podcastInfo}>
  <img
    src={currentPodcast.image}
    alt={currentPodcast.title}
    className={styles.podcastImage}
  />
  <div className={styles.podcastDetails}>
    <h3>{currentPodcast.title}</h3>
    <p>{currentPodcast.pubDate}</p>
  </div>
</div>

Close Button

Allows users to close the player:
<button onClick={onClose} className={styles.closeButton}>
  ×
</button>

Null State

Returns null when no podcast is selected:
if (!currentPodcast) return null;

Usage Example

import React, { useState } from "react";
import PersistentPlayer from "./components/PersistentPlayer/PersistentPlayer";
import { useSelector } from "react-redux";

function App() {
  const [playerVisible, setPlayerVisible] = useState(true);
  const { currentPodcast } = useSelector((state) => state.player);

  return (
    <div className="app">
      {/* Main content */}
      
      {/* Persistent player */}
      {currentPodcast && playerVisible && (
        <PersistentPlayer onClose={() => setPlayerVisible(false)} />
      )}
    </div>
  );
}

Audio Player Configuration

The react-h5-audio-player is configured with:
  • Jump controls: Skip forward/backward buttons
  • Stacked-reverse layout: Progress bar on top
  • Custom progress section: Current time, progress bar, duration
  • Loop control: Option to repeat episode
  • Volume control: Adjustable volume with persistence

Window Width Detection

Uses custom hook for responsive animations:
import useWindowWidth from "../../hooks/useWindowWidth";

const windowWidth = useWindowWidth();

// Used in animation initial position
initial={{ opacity: 0, x: windowWidth }}

Styling

The component uses CSS modules for scoped styling:
import styles from "./PersistentPlayer.module.css";

<div className={styles.persistentPlayer}>
  <div className={styles.podcastInfo}>...</div>
  <div className={styles.playerControls}>...</div>
  <button className={styles.closeButton}>×</button>
</div>
  • SleepTimer - Integrated timer component to stop playback after a set duration
  • PodcastDetail - Triggers player activation
  • MP3Player - Individual episode cards that can trigger playback

Build docs developers (and LLMs) love