问题 如何在React和Redux中处理音频播放


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

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

那么使用进度显示处理音频的最简单方法是什么?


2390
2018-03-09 12:14


起源



答案:


Redux - 它是关于状态和一致性的。

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

我看到两种可能的方法:

1.随时随地保持商店。

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

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

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

在时间更改时,您将分派操作并使用新时间更新商店。从而保持当前歌曲的时间,所有组件将保持同步。

使用操作分派,减少器和存储来管理单向数据流中的状态是Redux实现任何组件的方式。

这是#1 aproach的伪代码:

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.尽量少存储在商店中。

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

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

HTML5音频标记具有DOM事件,您可以保持两个组件同步而不触及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://files.freemusicarchive.org/music%2Fno_curator%2FThe_Womb%2FBang_-_An_Introduction_to_The_Womb%2FThe_Womb_-_02_-_Sex_Club.mp3";
		
		
		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>


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


13
2018-03-12 22:44



如果您需要同时拥有多个玩家,您可以为每个玩家提供一个唯一的ID,在所有操作中传递该ID,并将所有内容放在玩家ID下的商店中。 - OlliM
那就对了!它适用于这两种方法。 - Jordan Enev
非常感谢:)这个命令式设计似乎更好,因为这不会对性能产生太大影响。我仍会保留一些东西,但我仍然会使用状态来完成一些事情,比如赛道结束...... - Vinz243
另外,我如何从声明性代码传递到命令式代码。即,假设我有一个AudioComponent,它采用曲目名称,艺术家姓名和音频网址。如何在状态发生变化时确保不破坏该组件?我怎样才能听取状态变化?例如,我想改变音频源,我的父母会有类似的东西 <AudioPlayer audioURL={this.state.props.track.streamURL} /> 然后在我的代码中的某个地方,我将调用一个动作,它会使用reducer改变状态来改变URL。 - Vinz243
如果我正确理解VirtualDOM的工作方式,那么一些DOM元素会被状态改变所摧毁吗? - Vinz243


答案:


Redux - 它是关于状态和一致性的。

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

我看到两种可能的方法:

1.随时随地保持商店。

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

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

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

在时间更改时,您将分派操作并使用新时间更新商店。从而保持当前歌曲的时间,所有组件将保持同步。

使用操作分派,减少器和存储来管理单向数据流中的状态是Redux实现任何组件的方式。

这是#1 aproach的伪代码:

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.尽量少存储在商店中。

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

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

HTML5音频标记具有DOM事件,您可以保持两个组件同步而不触及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://files.freemusicarchive.org/music%2Fno_curator%2FThe_Womb%2FBang_-_An_Introduction_to_The_Womb%2FThe_Womb_-_02_-_Sex_Club.mp3";
		
		
		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>


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


13
2018-03-12 22:44



如果您需要同时拥有多个玩家,您可以为每个玩家提供一个唯一的ID,在所有操作中传递该ID,并将所有内容放在玩家ID下的商店中。 - OlliM
那就对了!它适用于这两种方法。 - Jordan Enev
非常感谢:)这个命令式设计似乎更好,因为这不会对性能产生太大影响。我仍会保留一些东西,但我仍然会使用状态来完成一些事情,比如赛道结束...... - Vinz243
另外,我如何从声明性代码传递到命令式代码。即,假设我有一个AudioComponent,它采用曲目名称,艺术家姓名和音频网址。如何在状态发生变化时确保不破坏该组件?我怎样才能听取状态变化?例如,我想改变音频源,我的父母会有类似的东西 <AudioPlayer audioURL={this.state.props.track.streamURL} /> 然后在我的代码中的某个地方,我将调用一个动作,它会使用reducer改变状态来改变URL。 - Vinz243
如果我正确理解VirtualDOM的工作方式,那么一些DOM元素会被状态改变所摧毁吗? - Vinz243