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
Callback function triggered when the close button is clicked
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:
Player Slice
AudioTime Slice
Podcast Slice
const {
currentPodcast, // Current podcast object
isPlaying // Playback state boolean
} = useSelector((state) => state.player);
const {
playbackTimes, // Object mapping titles to times
savePlaybackTime // Boolean setting
} = useSelector((state) => state.audioTime);
const {
completedEpisodes // Array of completed episode titles
} = useSelector((state) => state.podcast);
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
}}
>
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>
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