Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle Audio playing in React & Redux

I am making an audio player. It has pause, rewind and time seek features. How and who should handle the audio element?

  • I can put it aside the store. I cant put it directly on the state, since it might get cloned. Then when in the reducer I can interact with it. The problem is that if I need to sync the time slider with the audio I will need to constantly poll the store using an action. It also doesn't really makes sense semantically speaking.
  • I can create a custom React component, Audio, which does everything I said. the problem isn't solved. How can I refresh the slider? I could poll but I really don't like this solution. Besides, unless I create a component that contains both audio and slider, I would still need to use redux to connect both.

So what is the most redux way to handle audio with progress display?

like image 707
Vinz243 Avatar asked Mar 09 '17 12:03

Vinz243


People also ask

How do I play MP3 files in React?

To play an mp3 clip on click in React, we can use the Audio constructor to create an audio element with the MP3 file URL. Then we call play on the created object to play the audio clip. We create the audio object with the Audio constructor with the MP3 file URL as its argument. Then we call audio.

How do you listen to events on React?

To use the addEventListener method in function components in React: Set the ref prop on the element. Use the current property on the ref to get access to the element. Add the event listener in the useEffect hook.


1 Answers

Redux - it's all about the state and consistency.

Your goal is to keep in sync the song time and the progress bar.

I see two possible aproaches:

1. Keep everyting in the Store.

So you have to keep the song's current time (in seconds for instance) in the Store, because of there are a few dependend components and its hard to sync them without the Store.

You have few events those change the current time:

  • The current time itself. For example on each 1 second.
  • Rewind.
  • Time seek.

On a time change you will dispatch an action and will update the Store with the new time. Thereby keeping current song's time all components will be in sync.

Managing the state in unidirectional data flow with actions dispatching, reducers and stores is the Redux way of implementing any component.

Here is a pseudo code of the #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. Keep as less as possible in the Store.

If the above aproach doesn't fit your needs, for example you may have a lot of Audio players on a same screen - there may be a performance gap.

In that case you can access your HTML5 audio tag and components via refs in the componentDidMount lifecycle method.

The HTML5 audio tag has DOM events and you can keep the both components in sync without touching the Store. If there is a need to save something in the Store - you can do it anytime.

Please take a look at react-audio-player source code and check how it handles the refs and what API the plugin exposes. For sure you can take inspiration from there. Also you can reuse it for your use case.

Here are some of the API methods those are related to your questions:

  • onSeeked - Called when the user drags the time indicator to a new time. Passed the event.
  • onPlay - Called when the user taps play. Passed the event.
  • onPause - Called when the user pauses playback. Passed the event.
  • onListen - Called every listenInterval milliseconds during playback. Passed the event.

What approach should I use?

It depends to your use case specifics. However generally speaking in the both aproaches it's a good idea to implement a presentational component with the necessary API methods and it's up to you to decide how much data to manage in the Store.

So I created a starting component for you to illustrate how to handle the refs to the audio and slider. Start / Stop / Seeking features included. For sure it has drawbacks, but as I already mentioned it's a good starting point.

You can evolve it to a presentational component with good API methods, that suits your needs.

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>

If you have any questions feel free to comment below! :)

like image 160
Jordan Enev Avatar answered Oct 24 '22 10:10

Jordan Enev