如何在 React & Redux 中处理音频播放

IT技术 reactjs audio redux
2021-05-17 07:12:07

我正在制作音频播放器。它具有暂停、倒带和时间搜索功能。如何以及谁应该处理音频元素?

  • 我可以把它放在商店里。我不能把它直接放在状态上,因为它可能会被克隆。然后当在减速器中时,我可以与它进行交互。问题是,如果我需要将时间滑块与音频同步,我将需要使用操作不断轮询商店。从语义上讲,它也没有真正的意义。
  • 我可以创建一个自定义的 React 组件 Audio,它可以完成我所说的一切。问题没有解决。如何刷新滑块?我可以投票,但我真的不喜欢这个解决方案。此外,除非我创建一个包含音频和滑块的组件,否则我仍然需要使用 redux 来连接两者。

那么处理带有进度显示的音频的最redux 方式是什么?

2个回答

Redux - 一切都与状态和一致性有关。

您的目标是使歌曲时间和进度条保持同步。

我看到两种可能的方法:

1. 把所有东西都放在商店里。

所以你必须将歌曲的当前时间(例如以秒为单位)保存在 Store 中,因为有一些依赖组件并且没有 Store 很难同步它们。

您几乎没有更改当前时间的事件:

  • 当前时间本身。例如每 1 秒。
  • 倒带。
  • 时间寻找。

在时间更改时,您将发送一个操作并使用新时间更新 Store。从而保持当前歌曲的时间所有组件将同步。

在单向数据流中使用动作分派、reducer 和 store 管理状态是实现任何组件的 Redux 方式。

这是#1 方法的伪代码:

class AudioPlayer extends React.Component {

    onPlay(second) {
        // Store song current time in the Store on each one second
        store.dispatch({ type: 'SET_CURRENT_SECOND', second });
    }

    onRewind(seconds) {
        // Rewind song current time
        store.dispatch({ type: 'REWIND_CURRENT_SECOND', seconds });
    }   

    onSeek(seconds) {
        // Seek song current time
        store.dispatch({ type: 'SEEK_CURRENT_SECOND', seconds });
    }

    render() {
        const { currentTime, songLength } = this.state;

        return <div>
            <audio onPlay={this.onPlay} onRewind={this.onRewind} onSeek={this.onSeek} />

            <AudioProgressBar currentTime songLength />
        </div>
    }
}

2. 尽量少存放在商店里。

如果上述方法不符合您的需求,例如您可能在同一个屏幕上有很多音频播放器 - 可能存在性能差距。

在这种情况下,您可以通过componentDidMount 生命周期方法中的引用访问您的 HTML5 音频标签和组件

HTML5 音频标签具有 DOM 事件,您可以在不接触 Store 的情况下使两个组件保持同步。如果需要在 Store 中保存某些内容 - 您可以随时进行。

请查看react-audio-player 源代码并检查它如何处理 refs 以及插件公开的 API。当然,您可以从那里获得灵感。您也可以在您的用例中重复使用它。

以下是与您的问题相关的一些 API 方法:

  • onSeeked - 当用户将时间指示器拖动到新时间时调用。通过事件。
  • onPlay - 当用户点击播放时调用。通过事件。
  • onPause - 当用户暂停播放时调用。通过事件。
  • onListen - 在播放过程中每隔 listenInterval 毫秒调用一次。通过事件。

我应该使用什么方法?

这取决于您的用例细节。然而,一般来说,在这两种方法中,使用必要的 API 方法实现表示组件是一个好主意,并且由您决定要在 Store 中管理多少数据。

因此,我为您创建了一个起始组件来说明如何处理对音频和滑块的引用。包括开始/停止/寻找功能。它肯定有缺点,但正如我已经提到的,这是一个很好的起点。

您可以将其发展为具有良好 API 方法的表示组件,以满足您的需求。

class Audio extends React.Component {
	constructor(props) {
		super(props);
		
		this.state = {
			duration: null
		}
  	};
	
	handlePlay() {
		this.audio.play();
	}
	
	handleStop() {
		this.audio.currentTime = 0;
		this.slider.value = 0;
		this.audio.pause(); 
	}

	componentDidMount() {
		this.slider.value = 0;
		this.currentTimeInterval = null;
		
		// Get duration of the song and set it as max slider value
		this.audio.onloadedmetadata = function() {
			this.setState({duration: this.audio.duration});
		}.bind(this);
		
		// Sync slider position with song current time
		this.audio.onplay = () => {
			this.currentTimeInterval = setInterval( () => {
				this.slider.value = this.audio.currentTime;
			}, 500);
		};
		
		this.audio.onpause = () => {
			clearInterval(this.currentTimeInterval);
		};
		
		// Seek functionality
		this.slider.onchange = (e) => {
			clearInterval(this.currentTimeInterval);
			this.audio.currentTime = e.target.value;
		};
	}

	render() {
		const src = "https://mp3.gisher.org/download/1000/preview/true";
		
		
		return <div>
			<audio ref={(audio) => { this.audio = audio }} src={src} />
			
			<input type="button" value="Play"
				onClick={ this.handlePlay.bind(this) } />
			
			<input type="button"
					value="Stop"
					onClick={ this.handleStop.bind(this) } />
			
			<p><input ref={(slider) => { this.slider = slider }}
					type="range"
					name="points"
					min="0" max={this.state.duration} /> </p>
		</div>
	}
}

ReactDOM.render(<Audio />, document.getElementById('container'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="container">
    <!-- This element's contents will be replaced with your component. -->
</div>


如果您有任何问题,请随时在下面发表评论!:)

简单的react解决方案

为了使音频播放器与应用程序的其余部分保持同步,您可以使用经典的 React props down events up data flow。这意味着您将状态保留在父组件中,并将其作为props以及修改状态的事件处理程序传递给子组件。更具体地说,在您所在的州,您可以拥有:

  • seekTime:这将用于强制更新您的播放器的时间
  • appTime:这将由您的播放器广播并传递给其他组件,以使它们与播放器保持同步。

示例实现可能如下所示:

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

function App() {
  // we'll define state in parent component:
  const [playing, setPlaying] = useState(false);
  const [duration, setDuration] = useState(0);
  const [seekTime, setSeekTime] = useState(0); // forces player to update its time
  const [appTime, setAppTime] = useState(0); // dictated by player, broadcasted to other components

  // state will be passed as props and setter function will allow child components to change state:
  return (
    <div>
      <button onClick={() => setPlaying(true)}>PLAY</button>
      <button onClick={() => setPlaying(false)}>PAUSE</button>
      <button onClick={() => setSeekTime(appTime - 5)}>-5 SEC</button>
      <button onClick={() => setSeekTime(appTime + 5)}>+5 SEC</button>
      <Seekbar
        value={appTime}
        min="0"
        max={duration}
        onInput={(event) => setSeekTime(event.target.value)}
      />
      <Player
        playing={playing}
        seekTime={seekTime}
        onTimeUpdate={(event) => setAppTime(event.target.currentTime)}
        onLoadedData={(event) => setDuration(event.target.duration)}
      />
    </div>
  );
}

function Seekbar({ value, min, max, onInput }) {
  return (
    <input
      type="range"
      step="any"
      value={value}
      min={min}
      max={max}
      onInput={onInput}
    />
  );
}

function Player({ playing, seekTime, onTimeUpdate, onLoadedData }) {
  const ref = useRef(null);
  if (ref.current) playing ? ref.current.play() : ref.current.pause();
  //updates audio element only on seekTime change (and not on each rerender):
  useEffect(() => (ref.current.currentTime = seekTime), [seekTime]);
  return (
    <audio
      src="./your.file"
      ref={ref}
      onTimeUpdate={onTimeUpdate}
      onLoadedData={onLoadedData}
    />
  );
}

export default App;

类似 Redux 的解决方案

如果您更喜欢类似 redux 的解决方案,您可以将应用程序的状态移动到 reducer 函数并像这样重写父组件:

import { useRef, useEffect, useReducer } from "react";

// define reducer and initial state outside of component
const initialState = { playing: false, duration: 0, seekTime: 0, appTime: 0 };
function reducer(state, action) {
  switch (action.type) {
    case "play":
      return { ...state, playing: true };
    case "pause":
      return { ...state, playing: false };
    case "set-duration":
      return { ...state, duration: action.value };
    case "set-time":
      return { ...state, appTime: action.value };
    case "seek":
      return { ...state, seekTime: action.value };
    default:
      throw new Error("Unhandled action " + action.type);
  }
}

function App() {
  // use reducer and dispatch instead of state
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <button onClick={() => dispatch({ type: "play" })}>PLAY</button>
      <button onClick={() => dispatch({ type: "pause" })}>PAUSE</button>
      <button
        onClick={() => dispatch({ type: "seek", value: state.appTime + 5 })}
      >
        -5 SEC
      </button>
      <button
        onClick={() => dispatch({ type: "seek", value: state.appTime + 5 })}
      >
        +5 SEC
      </button>
      <Seekbar
        value={state.appTime}
        min="0"
        max={state.duration}
        onInput={(event) =>
          dispatch({ type: "seek", value: event.target.value })
        }
      />
      <Player
        playing={state.playing}
        seekTime={state.seekTime}
        onTimeUpdate={(event) =>
          dispatch({ type: "set-time", value: event.target.currentTime })
        }
        onLoadedData={(event) =>
          dispatch({ type: "set-duration", value: event.target.duration })
        }
      />
    </div>
  );
}

// The rest of app doesn't need any change compared to previous example. 
// That's due to decoupled architecture!
function Seekbar({ value, min, max, onInput }) {
  return (
    <input
      type="range"
      step="any"
      value={value}
      min={min}
      max={max}
      onInput={onInput}
    />
  );
}

function Player({ playing, seekTime, onTimeUpdate, onLoadedData }) {
  const ref = useRef(null);
  if (ref.current) playing ? ref.current.play() : ref.current.pause();
  useEffect(() => (ref.current.currentTime = seekTime), [seekTime]);
  return (
    <audio
      src="./your.file"
      ref={ref}
      onTimeUpdate={onTimeUpdate}
      onLoadedData={onLoadedData}
    />
  );
}

export default App;

在应用程序周围传递 DOM 引用?

简单地将 ref 传递给应用程序周围的 DOM 音频元素,而不是实现适当的状态管理,这可能很诱人。但是,此解决方案会将您的组件耦合在一起,从而使您的应用程序更难维护。因此,除非您真的真的需要 React 的虚拟 DOM 花费的 3 毫秒(并且在大多数情况下您不需要),否则我建议不要这样做