【问题标题】:Playing sound in React.js在 React.js 中播放声音
【发布时间】:2018-05-21 01:06:07
【问题描述】:
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

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

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

我不确定为什么会发生这种情况,因为我没有看到任何未定义的状态。一种;;状态已被宣布。

我是新手,所以我可能会错过一些非常重要的东西。

请帮忙!

【问题讨论】:

    标签: javascript reactjs


    【解决方案1】:

    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 版本(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;
    

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

    回应@Cold_Class 的评论:

    不幸的是,如果我使用多个这些组件,那么每当我开始播放另一个组件时,来自其他组件的音乐都不会停止播放 - 有什么关于此问题的简单解决方案的建议吗?

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

    一种解决方案是修改钩子本身以同时管理多个音频源。这是一个示例实现:

    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
    

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

    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
    

    这个想法是管理2个并行数组:

    • 您的音频源(由您传递给父组件的 urls 道具构建;urls 道具是一个字符串数组(您的 MP3 URL))
    • 一个跟踪每个玩家状态的数组

    toggle 方法根据以下逻辑更新播放器状态数组:

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

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

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

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

    最后,从父MultiPlayer 组件(包含多个播放器)调用新钩子,然后使用 (a) 包含播放器当前状态及其源流 URL 的对象映射到单个 Players (b) 使用玩家索引柯里化的切换方法。

    CodeSandbox demo

    【讨论】:

    • @Dawn17 不客气 - 如果有帮助,请同时对您的代码进行一些更改;)
    • 感谢您的帮助!顺便说一句,我最终试图实现一个音乐列表。所以,我可能想让this.url 成为mp3 网址的[ ]。您认为按顺序播放歌曲是否可行?
    • @Dawn 我不知道音频播放源序列的能力 - 您必须查找您正在使用的任何音频对象/库的文档。但是,我会将该 URL 列表移动到状态中并从那里进行管理(并且还有一个名为 currentUrl 的状态变量),并实现一个 next 方法以从一个 URL 跳到下一个 URL。祝你好运!
    • @Dawn17 实际上,考虑一下,将 URL 列表作为道具传递给您的组件会更有意义,例如 &lt;Music playlist={urls} /&gt; 其中 urls 是您的数组。然后您的 url 列表将在 this.props.playlist 下的组件中可用。在这种状态下,我将跟踪当前播放的 URL 为this.state.current
    • 谢谢,这是有道理的!顺便说一句,你知道如何停止音乐而不是暂停吗?
    【解决方案2】:

    我在这个答案的实现中遇到了一个不同的问题。

    似乎浏览器在每次重新渲染时都在不断尝试下载声音。

    我最终将useMemo 用于没有依赖关系的音频,这导致钩子只创建一次音频并且从不尝试重新创建它。

    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;
    
    

    【讨论】:

      【解决方案3】:

      在使用 Next Js 时,我在执行这些步骤时遇到了一些问题,因为 Audio 是 HTMLElement 标签,最终,它给我带来了一个很大的错误,所以我决定研究更多,在我的项目中它的结果如下:

        //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);
      

      为了处理播放器,我做了一个useEffect:

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

      您将根据您目前所做的功能管理状态“正在播放”。

      【讨论】:

      • 为什么音频必须使用 useState ?
      • 因为客户端可以随时对其进行变异...基本上,“useEffect”正在监视isPlaying 状态..在音频情况下,我明确表示它的类型是因此,音频 HTMLElement 类型触发isPlaying 将导致对该audio 状态的反应,无论是否播放...但主要答案是它是一个状态,因为我正在更改它在客户端上的值一边。
      • 你不应该把它放在 useState 中。可单独定义,Play/Mute 应保持在该状态。这是不好的做法。
      • 我鼓励您发布您的解决方案版本。
      【解决方案4】:

      一个简单的方法是使用useSound 钩子。

      为此,首先安装 npm 包:

      npm install use-sound
      

      进口:

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

      使用示例:

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

      注意 1:如果您想在函数中重现声音,您需要使用 playSound()

      注意2:更多信息click herehere

      【讨论】:

        【解决方案5】:

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

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

        如果您在play() 中执行console.log(this),您将看到this 未定义,这就是它抛出该错误的原因,因为您正在执行this.setState()。基本上this 的值在@987654328 中@ 取决于如何调用该函数。

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

        1. 使用bind() 设置函数的值,不管它是如何调用的:
        constructor(props) {
          super(props);
          this.play() = this.play.bind(this);
        }
        
        1. 使用不提供自己的 this 绑定的箭头函数
        <button onClick={() => {this.play()}}>Play</button>
        

        现在您可以在play() 中访问this.setStatethis.audiopause() 也是如此。

        【讨论】:

          【解决方案6】:

          我在这里参加派对有点晚了,但我从“Thomas Hennes”中退缩了:

          查看此内容的人会遇到的一个问题是,如果您尝试在具有多个页面的应用程序中逐字使用此代码,他们将不会玩得很开心。由于状态是在组件中管理的,因此您可以再次播放、导航和播放。

          要解决这个问题,您希望让组件将其状态推送到 App.js 并在那里管理状态。

          请允许我表明我的意思。

          我的播放器组件如下所示:

          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;
          

          然后在我的 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;
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2019-01-09
            • 2015-05-02
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多