在 React.js 中播放声音

IT技术 javascript reactjs
2021-04-09 04:44:55
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”

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

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

请帮忙!

6个回答

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;

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

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

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

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

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

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

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

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

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

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

代码沙盒演示

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

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

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

我最终使用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;

在使用 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]);

您将根据到目前为止所做的功能管理状态“isPlaying”。

因为客户端可以随时改变它......基本上,“useEffect”正在监视isPlaying状态......在音频的情况下,我让它明确它的类型是音频 HTMLElement 类型,因此,触发isPlaying将导致对该audio状态的反应,无论是否播放...但主要的答案是它是一种状态,因为我正在客户端更改其值。
2021-05-22 04:44:55
你不应该把它放在 useState 中。可以单独定义,Play/Mute 应该保持在状态。这是不好的做法。
2021-06-05 04:44:55
为什么音频必须使用 useState ?
2021-06-09 04:44:55

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

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

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

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

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

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

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