81
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!

Penny Liu
  • 15,447
  • 5
  • 79
  • 98
Dawn17
  • 7,825
  • 16
  • 57
  • 118

7 Answers7

164

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:

  • 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

Thomas Hennes
  • 9,023
  • 3
  • 27
  • 36
  • @Dawn17 you're welcome - made a couple of changes to your code in the meantime if that helps ;) – Thomas Hennes Dec 07 '17 at 02:21
  • Thanks that might help! Btw, I am eventually trying to implement a list of music. So, I might want to make the `this.url` as a `[ ]` of urls of mp3s. Do you think it will be doable to play the song sequentially? – Dawn17 Dec 07 '17 at 02:27
  • @Dawn I don't know about the ability for the audio to play a sequence of sources - you'll have to look up the documentation for whatever audio object/library you are using. I would, however, move that list of URLs inside the state and manage it from there (and also have a state variable named `currentUrl`), and implement a `next` method to skip from one URL to the next. Good luck! – Thomas Hennes Dec 07 '17 at 02:37
  • @Dawn17 Actually, thinking about it, it would make more sense to pass the list of URLs as a prop to your component, like `` where `urls` is your array. Then your list of urls would be available in the component under `this.props.playlist`. And in the state I would track the current URL being played as `this.state.current`. – Thomas Hennes Dec 07 '17 at 02:44
  • Thanks that makes sense! Btw do you have an idea how to stop the music instead of pausing? – Dawn17 Dec 07 '17 at 02:49
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/160669/discussion-between-jaxx-and-dawn17). – Thomas Hennes Dec 07 '17 at 02:50
  • This is great! 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? – Cold_Class Mar 15 '20 at 13:07
  • @Cold_Class There is no straightforward solution using the exact same codebase, but you can adapt the code to address managing multiple concurrent players. See my updated answer for a possible solution (tested locally) :) – Thomas Hennes Mar 16 '20 at 18:37
  • @Cold_Class Added a link to a working multiple players CodeSandbox demo at the end of the updated answer. – Thomas Hennes Mar 16 '20 at 18:56
  • I get `'audio' is not defined` in the `componentDidMount` and `componentWillUnmount` methods for examples. – h0r53 Mar 07 '22 at 00:34
  • Hey, thanks a lot for this snippet. I have faced a new problem, for Mobile devices, when the screen is locked the sound doesn't play. Do you know any workaround for it? – uiltonsantos Jan 04 '23 at 17:03
  • in your hooks example, why do you use `const [audio] = useState(new Audio(url));` instead of `const audio = new Audio(url);`? – Frazer Kirkman Feb 19 '23 at 13:02
  • is it because the `useAudio` will be called multiple times, and `useState(new Audio(url));` ensures the Audio is only created once? – Frazer Kirkman Feb 19 '23 at 13:12
12

You can also accomplish this by using the useSound hook.

To do this, first install the npm package:

npm install use-sound

Imports:

import useSound from 'use-sound'
import mySound from '../assets/sounds/yourSound.mp3' // Your sound file path here

Usage example 1

A simple approach..

function MyButton(){
  const [playSound] = useSound(mySound)
  
  return (
    <button onClick={() => playSound()}>
       Play Sound
    </button>
  )
}

Usage example 2

In this setup we can control the volume. Also, playSound() will be called inside the handleClick() function, allowing you to do more things on click than just playing a sound.

function MyButton(){
  const [playSound] = useSound(mySound, { volume: 0.7 }) // 70% of the original volume
  
  const handleClick = () => {
    playSound()
    // maybe you want to add other things here?
  }

  return (
    <button onClick={() => handleClick()}>
       Play Sound
    </button>
  )
}

For more info click here or here

Gass
  • 7,536
  • 3
  • 37
  • 41
  • 1
    Thank you this was so quick and easy to set up! Note for others, I ended up using const myAudio = require("./audio.mp3") which made TypeScript happy – Dal May 10 '22 at 22:16
  • 2
    really clean solution! for typescript just added a .d.ts file with `declare module "*.mp3"; declare module "*.wav";` as pointed out here: https://stackoverflow.com/a/73037254/1787312 and works like a charm – Facundo Colombier Jul 19 '22 at 13:12
6

I faced a different problem with this implementation of the answer.

It seemed the browser was continuously trying to download the sound on every re-render.

I ended up using useMemo for the Audio with no dependencies which causes the hook to only ever once create the Audio and never attempt to recreate it.

import {useMemo, useEffect, useState} from "react";

const useAudio = url => {
    const audio = useMemo(() => 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];
};

export default useAudio;

Titan Chase
  • 101
  • 1
  • 6
2

I got some problems following these steps when working with Next Js because Audio is HTMLElement tag, eventually, it was rendering me a big fat error, so I decided to study more and the result for it in my project was the following:

  //inside your component function.
  const [audio] = useState( typeof Audio !== "undefined" && new Audio("your-url.mp3")); //this will prevent rendering errors on NextJS since NodeJs doesn't recognise HTML tags neither its libs.
  const [isPlaying, setIsPlaying] = useState(false);

To handle the player, I made a useEffect:

    useEffect(() => {
    isPlaying ? audio.play() : audio.pause();
  }, [isPlaying]);

You will manage the state "isPlaying" according to the functions you make so far.

Code Drop
  • 79
  • 6
  • Why audio has to use useState ? – Hypothesis Jun 26 '21 at 16:35
  • because the client side can mutate it whenever he wants... basically, "useEffect" is watching over `isPlaying` state.. and in Audio case, i'm letting it explicit that it's type is an Audio HTMLElement type, therefore, triggering the `isPlaying` will cause a reaction on that `audio` state, to whether play it or not... but the main answer is that it's an state because i'm changing its value on client side. – Code Drop Jul 28 '21 at 15:06
  • You should not put that in a useState. It can be defined separately and the Play/ Mute should be kept in the state. This is bad practice. – Hypothesis Jul 28 '21 at 16:16
  • I encourage you to post your solution version. – Code Drop Feb 06 '22 at 00:17
1

I'm a bit late to the party here but piggy backing off of 'Thomas Hennes':

One problem people looking at this will run into is, if you try to use this code verbatim in an app with multiple pages, they are not going to have a nice time. Since state is managed at the component, you can play, navigate and play again.

To get around that you want to have your component push it's state up to App.js instead and manage the state there.

Allow me to show what I mean.

My player component looks like this:

import React, { Component } from 'react'

class MusicPlayer extends Component {
  render() {
    const { playing } = this.props.player;

    return (
      <div>
        <button onClick={this.props.toggleMusic.bind(this, playing)}>{playing ? "Pause" : "Play"}</button>
      </div>
    );
  }
};

export default MusicPlayer;

Then in my App.js it looks something like this (using a TODO list sample app):

import React, { Component } from 'react';
import { BrowserRouter as Router, Route  } from 'react-router-dom'
import './App.css';
import Header from './componets/layout/Header'
import Todos from './componets/Todos'
import AddTodo from './componets/AddTodo'
import About from './componets/pages/About'
import MusicPlayer from './componets/MusicPlayer'
import axios from 'axios';


class App extends Component {
  constructor(props) {
    super(props);
    this.state = { playing: false, todos: [] }
    this.audio = new Audio('<YOUR MP3 LINK HERE>');
  }

  componentDidMount(){
    axios.get('https://jsonplaceholder.typicode.com/todos')
      .then(res => this.setState({ playing: this.state.playing, todos: res.data }))
  }

  toggleComplete = (id) => {
    this.setState({ playing: this.state.playing, todos: this.state.todos.map(todo => {
      if (todo.id === id){
        todo.completed = !todo.completed
      }
      return todo
    }) });
  }

  delTodo = (id) => {
    axios.delete(`https://jsonplaceholder.typicode.com/todos/${id}`)
      .then(res => this.setState({ playing: this.state.playing, todos: [...this.state.todos.filter(todo => todo.id !== id)] }));
  }

  addTodo = (title) => {
    axios.post('https://jsonplaceholder.typicode.com/todos', {
      title,
      completed: false
    })
      .then(res => this.setState({ playing: this.state.playing, todos: [...this.state.todos, res.data]}))

  }

  toggleMusic = () => {
    this.setState({ playing: !this.state.playing, todos: this.state.todos}, () => {
      this.state.playing ? this.audio.play() : this.audio.pause();
    });
  }

  render() {
    return (
      <Router>
        <div className="App">
          <div className="container">
            <Header />
            <Route exact path="/" render={props => (
              <React.Fragment>
                <AddTodo addTodo={this.addTodo} />
                <Todos todos={this.state.todos} toggleComplete={this.toggleComplete} delTodo={this.delTodo} />
              </React.Fragment>
            )} />
            <Route path="/About" render={props => (
              <React.Fragment>
                <About />
                <MusicPlayer player={this.state} toggleMusic={this.toggleMusic} />
              </React.Fragment>
            )} />
          </div>
        </div>
      </Router>
    );
  }
}

export default App;
mBrice1024
  • 790
  • 5
  • 25
0

Uncaught TypeError: Cannot read property 'setState' of undefined

The error occurs because of how the this keyword works in JavaScript. I think the Audio should play just fine if we solve that issue.

If you do a console.log(this) inside play() you will see that this it is undefined and that's why it throws that error, since you are doing this.setState().Basically the value of this inside play() depends upon how that function is invoked.

There are two common solutions with React:

  1. Using bind() to set the value of a function's this regardless of how it's called:
constructor(props) {
  super(props);
  this.play() = this.play.bind(this);
}
  1. Using arrow functions which don't provide their own this binding
<button onClick={() => {this.play()}}>Play</button>

Now you will have access to this.setState and this.audio inside play(), and the same goes for pause().

Pablo Corso
  • 119
  • 2
  • 6
0

You can try this, it work on me

var tinung = `${window.location.origin}/terimakasih.ogg`;
                        var audio = document.createElement("audio");

                        audio.autoplay = true;
                        audio.load();
                        audio.addEventListener(
                            "load",
                            function() {
                                audio.play();
                            },
                            true
                        );
                        audio.src = tinung;
Rahmad Al Habib
  • 300
  • 4
  • 7