Javascript 在 reactjs 中播放声音

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/47686345/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 03:54:58  来源:igfitidea点击:

Playing sound in reactjs

javascriptreactjs

提问by Dawn17

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

这是我用来在我的 react 应用程序中使用 url (this.url) 播放声音的代码。当我按下播放按钮时,它给了我一个错误

Uncaught TypeError: Cannot read property 'setState' of undefined

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!

请帮忙!

回答by Thomas Hennes

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

我稍微改进了 Jaxx 的版本以包含一个,eventListener以便在音频结束时按钮重置。

ES6 class properties syntax

ES6 类属性语法

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+):

钩子版本(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

2020 年 3 月 16 日更新:多个并发玩家

In response to @Cold_Class's comment:

回应@Cold_Class 的评论:

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 Playercomponent. The reason is that you somehow have to hoist up single player states to a MultiPlayerparent component in order for the togglefunction to be able to pause other Players than the one you directly interacted with.

不幸的是,没有使用我们用来实现单个Player组件的确切代码库的直接解决方案。原因是您必须以某种方式将单个玩家状态提升到MultiPlayer父组件,以便该toggle功能能够暂停与您直接交互的玩家以外的其他玩家。

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.jsusing the MultiPlayercomponent:

App.js使用MultiPlayer组件的示例:

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:

这个想法是管理2个并行阵列:

  • your audio sources (built from the urlsprops you pass to the parent component ; the urlsprops is an array of strings (your MP3 URLs))
  • an array tracking the state of each player
  • 您的音频源(根据urls您传递给父组件的urls道具构建;道具是一个字符串数组(您的 MP3 URL))
  • 跟踪每个玩家状态的数组

The togglemethod updates the player state array based on the following logic:

toggle方法基于以下逻辑更新玩家状态数组:

  • 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 anotheraudio 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]
  • 如果当前有一个播放器处于活动状态(即正在播放音频)并且该活动播放器不是切换方法所针对的播放器,则将该播放器的播放状态恢复为 false,并将目标播放器的播放状态设置为 true [您点击了“播放” '而另一个音频流已经在播放]
  • 如果当前处于活动状态的玩家是切换方法所针对的玩家,只需将目标玩家的播放状态恢复为 false [您点击了“暂停”]
  • 如果当前没有播放器处于活动状态,只需将目标播放器的状态设置为 true [您在当前没有音频流播放时点击了“播放”]

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

请注意,该toggle方法被柯里化为接受源播放器的索引(即单击相应按钮的子组件的索引)。

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

实际的音频对象控制发生在useEffect原始钩子中,但稍微复杂一些,因为我们必须在每次更新时遍历整个音频对象数组。

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

类似地,音频流“结束”事件的事件侦听器useEffect在原始钩子中在一秒钟内处理,但更新为处理音频对象数组而不是单个此类对象。

Finally, the new hook is called from the parent MultiPlayercomponent (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.

最后,从父MultiPlayer组件(持有多个玩家)调用新的钩子,然后Player使用 (a) 一个包含玩家当前状态及其源流 URL 的对象和 (b) 与球员指数。

CodeSandbox demo

代码沙盒演示

回答by Pablo Corso

Uncaught TypeError: Cannot read property 'setState' of undefined

未捕获的类型错误:无法读取未定义的属性“setState”

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

发生此错误的原因是this关键字在 JavaScript 中的工作方式。如果我们解决了这个问题,我认为音频应该可以正常播放。

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

如果你做一个console.log(this)里面play()你会看到,this它是不确定的,这就是为什么它抛出错误,因为你正在做this.setState()的。基本上值thisplay()取决于该函数是如何调用。

There are two common solutions with React:

React 有两种常见的解决方案:

  1. Using bind()to set the value of a function's this regardless of how it's called:
  1. 使用bind()设置函数 this 的值,而不管它是如何调用的:
constructor(props) {
  super(props);
  this.play() = this.play.bind(this);
}
  1. Using arrow functions which don't provide their own this binding
  1. 使用不提供自己的 this 绑定的箭头函数
<button onClick={() => {this.play()}}>Play</button>

Now you will have access to this.setStateand this.audioinside play(), and the same goes for pause().

现在您将可以访问this.setStatethis.audioinside play(),对于pause().

回答by mBrice1024

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

我在这里参加派对有点晚了,但对“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.

为了解决这个问题,你想让你的组件将它的状态推送到 App.js 并在那里管理状态。

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):

然后在我的 App.js 中它看起来像这样(使用 TODO 列表示例应用程序):

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;