Playing sound in reactjs

前端 未结 3 2011
梦毁少年i
梦毁少年i 2020-12-02 15:54
import React, { Component } from \'react\'
import { Button, Input, Icon,Dropdown,Card} from \'semantic-ui-react\'
import { Link } from \'react-router-dom\'
import $          


        
3条回答
  •  南方客
    南方客 (楼主)
    2020-12-02 16:28

    I improved Jaxx's version slightly to include an eventListener so that the button resets when the audio ends.

    ES6 class properties syntax

    class Music extends React.Component {
      state = {
        play: false
      }
      audio = new Audio(this.props.url)
    
      componentDidMount() {
        audio.addEventListener('ended', () => this.setState({ play: false }));
      }
    
      componentWillUnmount() {
        audio.removeEventListener('ended', () => this.setState({ play: false }));  
      }
    
      togglePlay = () => {
        this.setState({ play: !this.state.play }, () => {
          this.state.play ? this.audio.play() : this.audio.pause();
        });
      }
    
      render() {
        return (
          
    ); } } export default Music;

    Hooks version (React 16.8+):

    import React, { useState, useEffect } from "react";
    
    const useAudio = url => {
      const [audio] = useState(new Audio(url));
      const [playing, setPlaying] = useState(false);
    
      const toggle = () => setPlaying(!playing);
    
      useEffect(() => {
          playing ? audio.play() : audio.pause();
        },
        [playing]
      );
    
      useEffect(() => {
        audio.addEventListener('ended', () => setPlaying(false));
        return () => {
          audio.removeEventListener('ended', () => setPlaying(false));
        };
      }, []);
    
      return [playing, toggle];
    };
    
    const Player = ({ url }) => {
      const [playing, toggle] = useAudio(url);
    
      return (
        
    ); }; export default Player;

    Update 03/16/2020: Multiple concurrent players

    In response to @Cold_Class's comment:

    Unfortunately if I use multiple of these components the music from the other components doesn't stop playing whenever I start another component playing - any suggestions on an easy solution for this problem?

    Unfortunately, there is no straightforward solution using the exact codebase we used to implement a single Player component. The reason is that you somehow have to hoist up single player states to a MultiPlayer parent component in order for the toggle function to be able to pause other Players than the one you directly interacted with.

    One solution is to modify the hook itself to manage multiple audio sources concurrently. Here is an example implementation:

    import React, { useState, useEffect } from 'react'
    
    const useMultiAudio = urls => {
      const [sources] = useState(
        urls.map(url => {
          return {
            url,
            audio: new Audio(url),
          }
        }),
      )
    
      const [players, setPlayers] = useState(
        urls.map(url => {
          return {
            url,
            playing: false,
          }
        }),
      )
    
      const toggle = targetIndex => () => {
        const newPlayers = [...players]
        const currentIndex = players.findIndex(p => p.playing === true)
        if (currentIndex !== -1 && currentIndex !== targetIndex) {
          newPlayers[currentIndex].playing = false
          newPlayers[targetIndex].playing = true
        } else if (currentIndex !== -1) {
          newPlayers[targetIndex].playing = false
        } else {
          newPlayers[targetIndex].playing = true
        }
        setPlayers(newPlayers)
      }
    
      useEffect(() => {
        sources.forEach((source, i) => {
          players[i].playing ? source.audio.play() : source.audio.pause()
        })
      }, [sources, players])
    
      useEffect(() => {
        sources.forEach((source, i) => {
          source.audio.addEventListener('ended', () => {
            const newPlayers = [...players]
            newPlayers[i].playing = false
            setPlayers(newPlayers)
          })
        })
        return () => {
          sources.forEach((source, i) => {
            source.audio.removeEventListener('ended', () => {
              const newPlayers = [...players]
              newPlayers[i].playing = false
              setPlayers(newPlayers)
            })
          })
        }
      }, [])
    
      return [players, toggle]
    }
    
    const MultiPlayer = ({ urls }) => {
      const [players, toggle] = useMultiAudio(urls)
    
      return (
        
    {players.map((player, i) => ( ))}
    ) } const Player = ({ player, toggle }) => (

    Stream URL: {player.url}

    ) export default MultiPlayer

    Example App.js using the MultiPlayer component:

    import React from 'react'
    import './App.css'
    import MultiPlayer from './MultiPlayer'
    
    function App() {
      return (
        
    ) } export default App

    The idea is to manage 2 parallel arrays:

    • your audio sources (built from the urls props you pass to the parent component ; the urls props is an array of strings (your MP3 URLs))
    • an array tracking the state of each player

    The toggle method updates the player state array based on the following logic:

    • if there is a player currently active (i.e. audio is playing) and this active player is not the player targeted by the toggle method, revert that player's playing state to false, and set the targeted player's playing state to true [you clicked on 'play' while another audio stream was already playing]
    • if the player currently active is the player targeted by the toggle method, simply revert the targeted player's playing state to false [you clicked on 'pause']
    • if there is no player currently active, simply set the targeted player's state to true [you clicked on 'play' while no audio stream was currently playing]

    Note that the toggle method is curried to accept the source player's index (i.e. the index of the child component where the corresponding button was clicked).

    Actual audio object control happens in useEffect as in the original hook, but is slightly more complex as we have to iterate through the entire array of audio objects with every update.

    Similarly, event listeners for audio stream 'ended' events are handled in a second useEffect as in the original hook, but updated to deal with an array of audio objects rather than a single such object.

    Finally, the new hook is called from the parent MultiPlayer component (holding multiple players), which then maps to individual Players using (a) an object that contains the player's current state and its source streaming URL and (b) the toggle method curried with the player's index.

    CodeSandbox demo

提交回复
热议问题