alexanderwallin / react-player-controls

⏯ Dumb and (re)useful React components for media players.
http://alexanderwallin.github.io/react-player-controls/
ISC License
191 stars 35 forks source link

can this work with hooks? And if so, what would i change? #50

Closed rchrdnsh closed 4 years ago

rchrdnsh commented 4 years ago

I have an app that is built with context and hooks, and i am curious wether i could used the progress slider with hooks, and useState, and if so, what would need to change...not quite sure how it works, really.

alexanderwallin commented 4 years ago

Have you run into any troubles, or are you wondering about hooks in general?

rchrdnsh commented 4 years ago

well, i have a music player for a Gatsby site, with everything built and working fine, except the progress slider and volume control...the player is based upon this article:

https://upmostly.com/tutorials/how-to-use-the-usecontext-hook-in-react

I have even managed how to figure out how to add the currentTime and duration as well, and I am now stuck on how to add a progress bar/ slider, which updates based on the progress of the current song, and then also allows the user to jump to any point in the song and the playback updates to that point.

The example is using context and hooks, with a custom useMusicPlayer hook, like so:

const useMusicPlayer = () => {
  const [state, setState] = useContext(MusicPlayerContext)

  // query all mp3 and jpg files from /content/music/
  const assets = useStaticQuery(graphql`
    query Assets {
      allFile(
        filter: {
          extension: { in: ["mp3", "jpg"] }
          absolutePath: { regex: "/content/music/" }
        }
      ) {
        edges {
          node {
            publicURL
            relativePath
          }
        }
      }
    }
  `)
  // convert to obj for fast lookup
  const assetObj = useMemo(
    () =>
      assets.allFile.edges.reduce((obj, file) => {
        const { publicURL, relativePath } = file.node

        obj[relativePath] = publicURL

        return obj
      }, {}),
    [assets]
  )

  // Play a specific track
  function playTrack(index) {
    if (index === state.currentTrackIndex) {
      togglePlay()
    } else {
      state.audioPlayer.pause()

      const base = state.tracks[index].audio.base // frost.mp3
      const baseName = basename(base) // frost

      // new Audio() does not support relative path
      // hence the need for window.location.origin
      const audioPlayer = new Audio(
        `${window.location.origin}${assetObj[`${baseName}/${base}`]}`
      ) // new Audio('http://www.domain.com/static/frost-[hash].mp3')

      audioPlayer.play()
      setState(state => ({
        ...state,
        currentTrackIndex: index,
        isPlaying: true,
        audioPlayer,
      }))
    }
  }

  // Toggle play or pause
  function togglePlay() {
    if (state.isPlaying) {
      state.audioPlayer.pause()
    } else {
      state.audioPlayer.play()
    }
    setState(state => ({ ...state, isPlaying: !state.isPlaying }))
  }

  // Play the previous track in the tracks array
  function playPreviousTrack() {
    const newIndex =
      (((state.currentTrackIndex + -1) % state.tracks.length) +
        state.tracks.length) %
      state.tracks.length
    playTrack(newIndex)
  }

  // Play the next track in the tracks array
  function playNextTrack() {
    const newIndex = (state.currentTrackIndex + 1) % state.tracks.length
    playTrack(newIndex)
  }

  // Transform the currentTime info into minutes and seconds
  function getTime(time) {
    if(!isNaN(time)) {
      return Math.floor(time / 60) + ':' + ('0' + Math.floor(time % 60)).slice(-2)
    }
  }

  const [currentTime, setCurrentTime] = useState(state.audioPlayer.currentTime)

  // both formattedTime and progress are states derived from audioPlayer.currentTime
  // so no need another useState
  const formattedTime = getTime(currentTime)
  const progress = currentTime / state.audioPlayer.duration

  useEffect(() => {
    const timeoutId= setInterval(() => {
      setCurrentTime(getTime(state.audioPlayer.currentTime))
      // setCurrentTime(formattedTime)
    }, 1000)
    return () => {
      // clean up function run when state.audioPlayer changes
      // reset currentTime to 0
      setCurrentTime(0)
      clearInterval(timeoutId)
    }
  }, [state.audioPlayer]);

  // get and display the duration of the track, in minutes and seconds

  // let currentDuration = state.audioPlayer.duration
  // let currentDuration = formatSecondsAsTime(Math.floor(state.audioPlayer.duration).toString())
  const duration = getTime(state.audioPlayer.duration)

  // jump to anywhere on the track using a progress bar style interface

  // adjust the volume of the track
  const volume = state.audioPlayer.duration.volume

  let currentTrackArtwork, currentTrackAudio

  if (state.currentTrackIndex !== null) {
    const base = state.tracks[state.currentTrackIndex].audio.base // frost.mp3
    const baseName = basename(base) // frost

    currentTrackArtwork =
      assetObj[
        `${baseName}/${state.tracks[state.currentTrackIndex].artwork.base}`
      ] // assetObj['frost/frost.jpg']
    currentTrackAudio =
      assetObj[
        `${baseName}/${state.tracks[state.currentTrackIndex].audio.base}`
      ] // assetObj['frost/frost.mp3']
  }

  return {
    playTrack,
    togglePlay,
    currentTrackName:
      state.currentTrackIndex !== null &&
      state.tracks[state.currentTrackIndex].name,
    currentTrackArtist:
      state.currentTrackIndex !== null &&
      state.tracks[state.currentTrackIndex].artist,
    currentTrackArtwork,
    currentTrackAudio,
    currentTime,
    duration,
    formattedTime,
    progress,
    volume,
    trackList: state.tracks,
    isPlaying: state.isPlaying,
    playPreviousTrack,
    playNextTrack,
  }
}

export default useMusicPlayer

and then I import these things into a PlayerControls.js file, like so:

const Controls = () => {

  const {
    isPlaying,
    currentTrackName,
    currentTrackArtwork,
    currentTime,
    duration,
    formattedTime,
    progress,
    volume,
    togglePlay,
    playPreviousTrack,
    playNextTrack
  } = useMusicPlayer()

  return (
    <>
      <FlexStart>
        <TrackArtwork src={currentTrackArtwork} alt="Album Artwork." />
        <TrackName>{currentTrackName}</TrackName>
      </FlexStart>
      <FlexCenter>
        <Button onClick={playPreviousTrack} disabled={!currentTrackName}>
          <Image src={previousButton} alt="Previous Button"/>
        </Button>
        <Button onClick={togglePlay} disabled={!currentTrackName}>
          {
            isPlaying ?
              <Image src={pauseButton} alt="Pause Button"/>
            :
              <Image src={playButton} alt="Play Button"/>
          }
        </Button>
        <Button onClick={playNextTrack} disabled={!currentTrackName}>
          <Image src={nextButton} alt="Next Button"/>
        </Button>
      </FlexCenter>
      <FlexCenter>

        <TrackDuration>{currentTime}</TrackDuration>

        // Progress Slider would go here...

        <TrackDuration>{duration}</TrackDuration>

       // Volume Slider would go here

      </FlexCenter>
    </>
  )
}

export default Controls

...so I'm looking at all the props with this in them and I don't really know what to do, or how to integrate your slider with what I have so far...

I know it's a bunch of code, but what a bunch of it is also doing is grabbing a bunch of data from some mdx files and creating the tracklist out of that mdx frontmatter and also grabbing the artworks and audio files as well, adding that all in to context.

If you are curious to look at the repo:

https://github.com/rchrdnsh/RYKR

I hope this makes sense, what I am stuck on. I just see a bunch of this.something, and I am wondering what I would need to do to adopt this component to what I have already (with much help from very smart people!) have built :-)

alexanderwallin commented 4 years ago

Well, generally what you need to do is manage your state somehow and then pass this state on as props to the <Slider /> or whichever component you need. Have you looked at the Progress bar with buffer recipe in the README? The <Slider /> is really just an invisible wrapper that provides mouse interaction helpers. What you want to render inside it is really up to you.

It sounds like you're fighting with hooks, and if you're new to them I would strongly recommend building something easier to begin with!