How can I map an audio player in React.JS and Hygraph?

37 views Asked by At

First of all, sorry for my english, it's not my first langage. Also I'm pretty new to coding so please be patient (and please be as clear as if you where talking to a rubber duck XD).

I'm trying to display mp3 from Hygraphs assets I uploaded. I want to map them so that if anybody add a file later, a new audioplayer will appear on my page.

The thing is my players are all playing the same mp3 even if I have several url, and/or I have errors such as

index.js:41 Uncaught TypeError: Cannot read properties of undefined (reading 'duration')

I think it's because of refs or something like that, but I don't know how to fix it at all.

My component:

function OnePodcast({ posts }) {
  const listOfPodcasts = posts.filter((post) => post.node.categories.some((category) => category.nom === 'Podcasts'));

  // State
  const [isPlaying, setIsPlaying] = useState(false);
  const [currentTime, setCurrentTime] = useState(0);
  const [duration, setDuration] = useState(0);

  // References
  const audioPlayer = useRef(); // reference the audioplayer component
  const progressBar = useRef(); // reference the progress bar
  const animationRef = useRef(); // reference the animation

  const onLoadedMetadata = () => {
    setDuration(Math.floor(audioPlayer.current.duration));
  };

  useEffect(() => {
    const seconds = Math.floor(audioPlayer.current.duration);
    setDuration(seconds);
    progressBar.current.max = seconds;
  }, [audioPlayer?.current?.loadedmetadata, audioPlayer?.current?.readyState]);

  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 togglePlayPause = () => {
    const prevValue = isPlaying;
    setIsPlaying(!prevValue);
    if (!prevValue) {
      audioPlayer.current.play();
      animationRef.current = requestAnimationFrame(whilePlaying);
    }
    else {
      audioPlayer.current.pause();
      cancelAnimationFrame(animationRef.current);
    }
  };

  const whilePlaying = () => {
    progressBar.current.value = audioPlayer.current.currentTime;
    changePlayerCurrentTime();
    animationRef.current = requestAnimationFrame(whilePlaying);
  };

  const changeRange = () => {
    audioPlayer.current.currentTime = progressBar.current.value;
    changePlayerCurrentTime();
  };

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

  const backThirty = () => {
    progressBar.current.value = Number(progressBar.current.value - 30);
    changeRange();
  };

  const forwardThirty = () => {
    const newTime = audioPlayer.current.currentTime + 30;
    // Needed this to fix the wrong number of secs added to the player
    if (newTime <= duration) {
      audioPlayer.current.currentTime = newTime;
      progressBar.current.value = newTime;
      changePlayerCurrentTime();
    }
  };

  return (
    <Container>
      {listOfPodcasts.map((podcast) => (
        <section className="onePodcast">
          <div className="audioPlayers-block">
            <article className="audioPlayer">
              <h3 className="header">{podcast.node.titre}</h3>
              <RichText
                content={podcast.node.contenu.raw}
              />
              <div className="audioPlayer__player">
                {/* Map to display assets files */}
                {podcast.node.fichier && podcast.node.fichier.map((fichier) => (
                  <audio ref={audioPlayer} src={fichier.url} preload="metadata" onLoadedData={onLoadedMetadata} />
                ))}
                {/* Buttons for desktop */}
                <div className="audioPlayer__player-btn displayNoneMobile">
                  <button type="button" onClick={backThirty} className="audioPlayer__btn"><TbPlayerTrackPrevFilled /> </button>
                  <button type="button" onClick={togglePlayPause} className="audioPlayer__main-btn">
                    {isPlaying ? <TbPlayerPauseFilled /> : <TbPlayerPlayFilled /> }
                  </button>
                  <button type="button" onClick={forwardThirty} className="audioPlayer__btn"><TbPlayerTrackNextFilled /> </button>
                </div>

                <div className="audioPlayer__player-bar">
                  {/* current time */}
                  <div className="audioPlayer__currentTime">{calculateTime(currentTime)}</div>
                  {/* Progress bar */}
                  <div>
                    <input type="range" className="audioPlayer__progressBar" defaultValue="0" ref={progressBar} onChange={changeRange} />
                  </div>
                  {/* duration */}
                  <div className="audioPlayer__duration">{(duration && !Number.isNaN(duration)) && calculateTime(duration)}</div>
                </div>

                {/* Buttons for mobile */}
                <div className="audioPlayer__player-btn displayNoneDesktop">
                  <button type="button" onClick={backThirty} className="audioPlayer__btn"><TbPlayerTrackPrevFilled /> </button>
                  <button type="button" onClick={togglePlayPause} className="audioPlayer__main-btn">
                    {isPlaying ? <TbPlayerPauseFilled /> : <TbPlayerPlayFilled /> }
                  </button>
                  <button type="button" onClick={forwardThirty} className="audioPlayer__btn"><TbPlayerTrackNextFilled /> </button>
                </div>
              </div>
            </article>
          </div>
        </section>
      ))}
    </Container>
  );
}

To be honest, I asked ChatGPT for help since I couldn't find anything online and here is the last code it proposed (with the same error "Uncaught TypeError: Cannot read properties of undefined (reading 'duration')"). Every other solutions it proposed were wrong the same way with a different error in "Cannot read properties of undefined (reading 'XXX').
Thank you soooo much for your help!

function OnePodcast({ posts }) {
  const listOfPodcasts = posts.filter((post) => post.node.categories.some((category) => category.nom === 'Podcasts'));

  // State
  const [isPlaying, setIsPlaying] = useState(false);
  const [currentTime, setCurrentTime] = useState(0);
  const [duration, setDuration] = useState(0);

  // References
  const audioPlayer = useRef(listOfPodcasts.map(() => createRef())); // reference the audioplayer component
  const progressBar = useRef(listOfPodcasts.map(() => createRef())); // reference the progress bar
  const animationRef = useRef(listOfPodcasts.map(() => null)); // initialize with null values

  const onLoadedMetadata = (index) => {
    setDuration(Math.floor(audioPlayer.current[index].current.duration));
  };

  useEffect(() => {
    const seconds = Math.floor(audioPlayer.current[0].current.duration);
    setDuration(seconds);
    progressBar.current[0].max = seconds;
  }, [audioPlayer?.current?.[0]?.loadedmetadata, audioPlayer?.current?.[0]?.readyState]);

  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 togglePlayPause = (index) => {
    const prevValue = isPlaying;
    setIsPlaying(!prevValue);
    const currentPlayer = audioPlayer.current[index].current;

    if (!prevValue) {
      currentPlayer.play();
      animationRef.current[index] = requestAnimationFrame(() => whilePlaying(index));
    } else {
      currentPlayer.pause();
      cancelAnimationFrame(animationRef.current[index]);
    }
  };

  const whilePlaying = (index) => {
    progressBar.current[index].current.value = audioPlayer.current[index].current.currentTime;
    changePlayerCurrentTime(index);
    animationRef.current[index] = requestAnimationFrame(() => whilePlaying(index));
  };

  const changeRange = (index) => {
    audioPlayer.current[index].current.currentTime = progressBar.current[index].current.value;
    changePlayerCurrentTime(index);
  };

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

  const backThirty = (index) => {
    progressBar.current[index].current.value = Number(progressBar.current[index].current.value - 30);
    changeRange(index);
  };

  const forwardThirty = (index) => {
    const newTime = audioPlayer.current[index].current.currentTime + 30;

    if (newTime <= duration) {
      audioPlayer.current[index].current.currentTime = newTime;
      progressBar.current[index].current.value = newTime;
      changePlayerCurrentTime(index);
    }
  };

  return (
    <Container>
      {listOfPodcasts.map((podcast, index) => (
        <section className="onePodcast" key={index}>
          <div className="audioPlayers-block">
            <article className="audioPlayer">
              <h3 className="header">{podcast.node.titre}</h3>
              <RichText content={podcast.node.contenu.raw} />
              <div className="audioPlayer__player">
                {/* map to display assets' files */}
                {podcast.node.fichier &&
                  podcast.node.fichier.map((fichier, index) => (
                    <audio
                      key={index}
                      ref={audioPlayer.current[index]}
                      src={fichier.url}
                      preload="metadata"
                      onLoadedData={() => onLoadedMetadata(index)}
                    />
                  ))}

                {/* desktop btn */}
                <div className="audioPlayer__player-btn displayNoneMobile">
                  <button type="button" onClick={() => backThirty(index)} className="audioPlayer__btn">
                    <TbPlayerTrackPrevFilled />{' '}
                  </button>
                  <button
                    type="button"
                    onClick={() => togglePlayPause(index)}
                    className="audioPlayer__main-btn"
                  >
                    {isPlaying ? <TbPlayerPauseFilled /> : <TbPlayerPlayFilled />}
                  </button>
                  <button type="button" onClick={() => forwardThirty(index)} className="audioPlayer__btn">
                    <TbPlayerTrackNextFilled />{' '}
                  </button>
                </div>

                <div className="audioPlayer__player-bar">
                  {/* current time */}
                  <div className="audioPlayer__currentTime">{calculateTime(currentTime)}</div>
                  {/* Barre de progression */}
                  <div>
                    <input
                      type="range"
                      className="audioPlayer__progressBar"
                      defaultValue="0"
                      ref={progressBar.current[index]}
                      onChange={() => changeRange(index)}
                    />
                  </div>
                  {/* duration */}
                  <div className="audioPlayer__duration">
                    {(duration && !Number.isNaN(duration)) && calculateTime(duration)}
                  </div>
                </div>

                {/* mobile btn */}
                <div className="audioPlayer__player-btn displayNoneDesktop">
                  <button type="button" onClick={() => backThirty(index)} className="audioPlayer__btn">
                    <TbPlayerTrackPrevFilled />{' '}
                  </button>
                  <button
                    type="button"
                    onClick={() => togglePlayPause(index)}
                    className="audioPlayer__main-btn"
                  >
                    {isPlaying ? <TbPlayerPauseFilled /> : <TbPlayerPlayFilled />}
                  </button>
                  <button type="button" onClick={() => forwardThirty(index)} className="audioPlayer__btn">
                    <TbPlayerTrackNextFilled />{' '}
                  </button>
                </div>
              </div>
            </article>
          </div>
        </section>
      ))}
    </Container>
  );
}
1

There are 1 answers

1
Alfredo L'huissier On

The bug in your React component arises because you're trying to use a single ref for multiple audio players. Refs in React are meant to reference a single DOM element, and when you use the same ref for a map function, it will only point to the last element in the map. Therefore, all controls end up controlling the last audio player, causing the behavior you're experiencing.

Try something like this:

function OnePodcast({ posts }) {
  const listOfPodcasts = posts.filter((post) =>
    post.node.categories.some((category) => category.nom === 'Podcasts')
  );

  // Create state hooks for each podcast
  const playersState = listOfPodcasts.map(() => ({
    isPlaying: false,
    currentTime: 0,
    duration: 0,
    audioPlayer: useRef(),
    progressBar: useRef(),
    animationRef: useRef(),
  }));

  const [players, setPlayers] = useState(playersState);

  const togglePlayPause = (index) => {
    const newPlayers = [...players];
    const currentPlayer = newPlayers[index];

    currentPlayer.isPlaying = !currentPlayer.isPlaying;
    setPlayers(newPlayers);

    if (currentPlayer.isPlaying) {
      currentPlayer.audioPlayer.current.play();
      currentPlayer.animationRef.current = requestAnimationFrame(() => whilePlaying(index));
    } else {
      currentPlayer.audioPlayer.current.pause();
      cancelAnimationFrame(currentPlayer.animationRef.current);
    }
  };

  // ... rest of your functions like whilePlaying, changeRange, etc. but use the index to reference the correct player

  return (
    <Container>
      {listOfPodcasts.map((podcast, index) => (
        <section key={index} className="onePodcast">
          {/* ... other parts of your component */}
          {/* Make sure to use `index` to reference the correct player state and ref */}
          {podcast.node.fichier && podcast.node.fichier.map((fichier, fileIndex) => (
            <audio
              key={fileIndex}
              ref={players[index].audioPlayer}
              src={fichier.url}
              preload="metadata"
              onLoadedData={() => onLoadedMetadata(index)}
            />
          ))}
          {/* ... */}
          <button type="button" onClick={() => togglePlayPause(index)} className="audioPlayer__main-btn">
            {players[index].isPlaying ? <TbPlayerPauseFilled /> : <TbPlayerPlayFilled />}
          </button>
          {/* ... */}
        </section>
      ))}
    </Container>
  );
}

This is a high-level example, and you will need to adjust your event handlers like onLoadedMetadata, whilePlaying, changeRange, etc., to accept the index parameter and use it to manipulate the state and refs of the correct audio player.

You would also need to make sure that you're updating the state correctly in an immutable way, which I've demonstrated in the togglePlayPause function with the spread operator [...players]. This ensures that you're not mutating the state directly but creating a new state array that React can use to determine what needs to be re-rendered.