import React, { Component } from 'react' import { Button, Input, Icon,Dropdown,Card} from 'semantic-ui-react' import { Link } from 'react-router-dom' import $ from 'jquery' import styles from './Home.scss' import Modal from './Modal.jsx' import MakeChannelModal from './MakeChannelModal.jsx' class Music extends React.Component { constructor(props) { super(props); this.state = { play: false, pause: true }; this.url = "http://streaming.tdiradio.com:8000/house.mp3"; this.audio = new Audio(this.url); } play(){ this.setState({ play: true, pause: false }); console.log(this.audio); this.audio.play(); } pause(){ this.setState({ play: false, pause: true }); this.audio.pause(); } render() { return ( <div> <button onClick={this.play}>Play</button> <button onClick={this.pause}>Pause</button> </div> ); } } export default Music
This is the code that I am using to play the sound with url (this.url) in my react app. When I press the play button, it gives me an error
Uncaught TypeError: Cannot read property 'setState' of undefined
I am not sure why this is happpening since I don't see any undefined states. A;; states have been declared.
I am new to react so I might be missing something very important.
Please help!
To play an mp3 clip on click in React, we can use the Audio constructor to create an audio element with the MP3 file URL. Then we call play on the created object to play the audio clip. We create the audio object with the Audio constructor with the MP3 file URL as its argument. Then we call audio.
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 ( <div> <button onClick={this.togglePlay}>{this.state.play ? 'Pause' : 'Play'}</button> </div> ); } } 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 ( <div> <button onClick={toggle}>{playing ? "Pause" : "Play"}</button> </div> ); }; 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 ( <div> {players.map((player, i) => ( <Player key={i} player={player} toggle={toggle(i)} /> ))} </div> ) } const Player = ({ player, toggle }) => ( <div> <p>Stream URL: {player.url}</p> <button onClick={toggle}>{player.playing ? 'Pause' : 'Play'}</button> </div> ) export default MultiPlayer
Example App.js
using the MultiPlayer
component:
import React from 'react' import './App.css' import MultiPlayer from './MultiPlayer' function App() { return ( <div className="App"> <MultiPlayer urls={[ 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3', 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3', 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3', ]} /> </div> ) } export default App
The idea is to manage 2 parallel arrays:
urls
props you pass to the parent component ; the urls
props is an array of strings (your MP3 URLs))The toggle
method updates the player state array based on the following logic:
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 Player
s 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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With