Skip to main content

Overview

The Nadie Sabe Nada podcast application features a robust audio player system with persistent playback, automatic progress tracking, and advanced controls. The player is built using React’s react-h5-audio-player library and integrates seamlessly with Redux state management.

Player Components

The audio player system consists of two main components:

Persistent Player

The persistent player appears as a slide-in panel that remains accessible across navigation. It provides full audio controls and information about the currently playing episode.
The persistent player automatically saves your playback position every second, so you can resume listening exactly where you left off.

Key Features

1

Automatic Resume

When you start playing an episode, the player automatically resumes from your last saved position
2

Volume Persistence

Volume settings are saved to localStorage and restored on your next visit
3

Playback Tracking

Progress is updated every second and synchronized with Redux state

Implementation Details

Playback Time Update

The player uses a throttled time update mechanism to avoid excessive state updates:
const handleTimeUpdate = (e) => {
    const currentTime = e.target.currentTime;
    const now = Date.now();

    if (now - lastTimeUpdateRef.current >= 1000) {
        dispatch(updatePlaybackTime({ title: currentPodcast.title, time: currentTime }));
        lastTimeUpdateRef.current = now;
    }
};
Playback position is only updated once per second to optimize performance and reduce Redux dispatches.

Volume Management

Volume settings persist across sessions using 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-Completion Detection

When an episode finishes playing, it’s automatically marked as completed:
const handleEnded = () => {
    dispatch(updatePlaybackTime({ title: currentPodcast.title, time: 0 }));
    dispatch(markAsCompleted(currentPodcast.title));
};

Player Controls

The audio player includes the following controls:
  • Play/Pause - Start or pause playback
  • Jump Controls - Skip forward or backward by set intervals
  • Volume Control - Adjust audio volume with persistent settings
  • Progress Bar - Seek to any point in the episode
  • Loop Control - Enable repeat playback
  • Sleep Timer - Schedule automatic playback stop (see Sleep Timer section below)

Sleep Timer

The sleep timer allows you to automatically stop playback after a specified duration.

Available Time Options

  • 5 minutes
  • 15 minutes (default)
  • 30 minutes
  • 45 minutes
  • 60 minutes (1 hour)

Using the Sleep Timer

1

Start Playback

Begin playing an episode first - the timer only works during active playback
2

Select Duration

Choose your desired duration from the dropdown menu
3

Activate Timer

Click the timer button to start the countdown
4

Monitor Progress

The timer displays remaining time in MM:SS format
The sleep timer will only activate if playback is in progress. Starting the timer while paused will show a warning message.

Timer Behavior

const handleTimerStart = () => {
    if (!isPlaying) {
        toast.custom(
            <div className={styles.confirmToast}>
                <div className={styles.confirmHeader}>
                    <Warning className={styles.warningIcon} />
                    <h3>¡Atención!</h3>
                </div>
                <p className={styles.confirmMessage}>
                    Inicia la reproducción primero para activar el temporizador.
                </p>
            </div>,
            { duration: 3000, position: isMobile ? "bottom-center" : "bottom-left" }
        );
        return;
    }
    setIsTimerActive(true);
    setTimeLeft(selectedTime * 60);
};

Auto-Cancellation

The sleep timer automatically cancels when:
  • An episode is marked as completed
  • The timer countdown reaches zero
  • You manually cancel it using the cancel button
useEffect(() => {
    if (currentPodcast && completedEpisodes.includes(currentPodcast.title) && isTimerActive) {
        handleTimerCancel(true);
    }
}, [completedEpisodes, currentPodcast, isTimerActive, handleTimerCancel]);

MP3 Player Cards

Each episode card on the main page includes quick audio controls:

Card Controls

  • Play/Pause Button - Start or pause the episode
  • Download Button - Download the episode for offline listening
  • Complete Button - Mark episode as completed
The play button icon changes based on playback state, showing a pause icon when the episode is currently playing.

Visual Indicators

Cards display visual status indicators:
  • Headphones Icon - Episode has been started (shows playback time on hover)
  • Checkmark Icon - Episode has been completed
  • Green Highlight - Episode is currently playing

State Management

The player uses Redux for state management with multiple slices:

Player Slice

Manages current playback state:
const handlePlay = () => {
    if (completedEpisodes.includes(currentPodcast.title)) {
        dispatch(removeFromCompleted(currentPodcast.title));
    }
    dispatch(togglePlay(true));
};

Audio Time Slice

Tracks playback positions for all episodes:
const { playbackTimes, savePlaybackTime } = useSelector((state) => state.audioTime);

const savedTime = savePlaybackTime ? playbackTimes[currentPodcast.title] || 0 : 0;
const audio = audioRef.current.audio.current;
audio.currentTime = savedTime;
Playback positions are stored per episode title, allowing you to have multiple episodes in progress simultaneously.

Best Practices

For optimal experience:
  • Allow the browser to access local storage for progress tracking
  • Use the sleep timer when listening before bed
  • Enable the “Save Playback Time” setting in Settings
  • Regularly mark completed episodes to keep your library organized

Animations

The persistent player features smooth animations powered by Framer Motion:
<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 }}
>
The player slides in from the right side of the screen with a spring animation for a smooth, natural feel.

Build docs developers (and LLMs) love