How to avoid NaN being rendered in audio player duration?

Viewed 256

I built a cool little audio player and am having issues with data fetching. The page renders before the audio file in the return statement src, here:

<audio ref={audio} src="https://dl.dropbox.com/s/wfhmtvbc5two1wa/1-allen_2991.ogg" alt="oops, something went wrong..."></audio>

The NaN shows up in the duration time represented by this line:

{/* duration */}
        <div className={styles.duration}>{(duration && !isNaN(duration)) && calculateTime(duration)}</div>  

This above line of code isn't preventing the NaN, so I tried my hand at fetching in the useEffect, shown below but that has made this issue worse.

const [data, setData] = useState([])
--------------------

useEffect(() => {
        fetch("https://dl.dropbox.com/s/wfhmtvbc5two1wa/1-allen_2991.ogg").then(
            res => setData(res.loadedmetadata)
        )
    })
--------------------

 <audio ref={audio} src={data} alt="oops, something went wrong..."></audio>

If anyone could give it a look and point me in the right direction, id be very grateful. Below I will provide all the code for my component.

import React, { useState, useRef, useEffect } from 'react';
import styles from '../styles/AudioPlayer.module.css';
import {BsArrowClockwise} from 'react-icons/bs';
import {BsArrowCounterclockwise} from 'react-icons/bs';
import {BsPlayCircleFill} from 'react-icons/bs';
import {BsPauseCircleFill} from 'react-icons/bs';

const AudioPlayer = () => {
  //state
  const [isPlaying, setIsPlaying] = useState(false);
  const [duration, setDuration] = useState(0);
  const [currentTime, setCurrentTime] = useState(0);
  const [data, setData] = useState([])

  //refs
  const audio = useRef();
  const progressBar = useRef();
  const progressBarAnimation = useRef(); 

  //effects
  useEffect(() => {
    const seconds = Math.floor(audio.current.duration);
    setDuration(seconds);
    progressBar.current.max = seconds;
  }, [ audio?.current?.loadedmetadata, audio?.current?.readyState ]);
 
    
    useEffect(() => {
        fetch("https://dl.dropbox.com/s/wfhmtvbc5two1wa/1-allen_2991.ogg").then(
            res => setData(res.loadedmetadata)
        )
    })
    

  //functions & Handlers
  const calculateTime = (secs) => {
    const minutes = Math.floor(secs / 60);
    const returnedMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`;
    const seconds = Math.floor(secs % 60);
    const returnedSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
    return `${returnedMinutes}:${returnedSeconds}`;
  }

  const isPlayingHandler = () => {
    const prevValue = isPlaying;
    setIsPlaying(!prevValue);
    if (!prevValue) {
      audio.current.play();
      progressBarAnimation.current = requestAnimationFrame(whilePlaying);
    } else {
      audio.current.pause();
      cancelAnimationFrame(progressBarAnimation.current);
    };
  };

  const whilePlaying = () => {
    progressBar.current.value = audio.current.currentTime;
    progressBarValueTicker();
    progressBarAnimation.current = requestAnimationFrame(whilePlaying);
  };

  const progressHandler = () => {
    audio.current.currentTime = progressBar.current.value;
    progressBarValueTicker();
  };

  const progressBarValueTicker = () => {
    progressBar.current.style.setProperty('--seek-before-width', `${progressBar.current.value / duration * 100}%`);
    setCurrentTime(progressBar.current.value);
  }

  const backwardFifteen = () => {
    console.log(progressBar.current.value)
    progressBar.current.value = Number(progressBar.current.value) - 15;
    console.log(progressBar.current.value)
    progressHandler();
    
  };

  const forwardFifteen = () => {
    console.log(progressBar.current.value)
    progressBar.current.value = Number(progressBar.current.value) + 15;
    console.log(progressBar.current.value)
    progressHandler();
  };

  return(
    <>
      <div>
         {/* eventually, a loop component tag will replace the below line to loop all audio file title and descriptions*/}
      </div>
      <div className={styles.audioWrapper}>
        {/* eventually, a loop component tag will replace the below line to loop all audio files*/}
        <audio ref={audio} src={data} alt="oops, something went wrong..."></audio>
        {/* <audio ref={audio} src="https://dl.dropbox.com/s/wfhmtvbc5two1wa/1-allen_2991.ogg" alt="oops, something went wrong..."></audio> */}
        <button className={styles.sideButtons} onClick={backwardFifteen}><BsArrowCounterclockwise />15</button>
        <button className={styles.playPauseButton} onClick={isPlayingHandler}>
          { isPlaying ? <BsPauseCircleFill /> : <BsPlayCircleFill /> }</button>
        <button className={styles.sideButtons} onClick={forwardFifteen}>15<BsArrowClockwise /></button>

        {/* current time */}
        <div className={styles.currentTime}>{calculateTime(currentTime)}</div>

        {/* progress bar */}
        <div>
          <input type="range" ref={progressBar} className={styles.progressBar} onChange={progressHandler} defaultValue='0'/>
        </div>

        {/* duration */}
        <div className={styles.duration}>{(duration && !isNaN(duration)) && calculateTime(duration)}</div>  
      </div>
    </>
  );
};

export default AudioPlayer;
2 Answers
    const onLoadedMetadata = ()=>{
        const seconds = Math.floor(audioPlayer.current.duration);
        setDuration(seconds);
        progressBar.current.max = seconds;
    }

<audio ref={audioPlayer} src={audio_file} preload="metadata" onLoadedMetadata={onLoadedMetadata}></audio>

The reason the NaN is rendered is because NaN is a falsy value and will return the value immediately in the expression below.

(duration && !isNaN(duration)) && calculateTime(duration)
// `NaN && !isNaN(NaN)` returns `NaN` because it is falsy

Simply removing the first condition will avoid NaN being rendered.

!isNaN(duration) && calculateTime(duration)

However, the actual duration value will still be NaN and nothing will get rendered. This is because when you check for the audio.current.duration value inside the useEffect the duration hasn't actually updated yet.

To solve this issue, you can listen to the onDurationChange event in the audio element and update the duration state variable when it gets triggered.

// Convert the `useEffect` code into a function instead
const onDurationChangeHandler = (e) => {
    const seconds = Math.floor(e.target.duration);
    setDuration(seconds);
    progressBar.current.max = seconds;
};
<audio
    ref={audio}
    src="https://dl.dropbox.com/s/wfhmtvbc5two1wa/1-allen_2991.ogg"
    alt="oops, something went wrong..."
    onDurationChange={onDurationChangeHandler}
></audio>
Related