I believe that we all have the need to develop music player, I am no exception, recently just do such a need, so by the way, the problems encountered and stepped on the pit, share ~
Because the project is based on React, the following source code is mostly extracted from the player component, but the logic is the same no matter what framework you develop in.
Player skin
Let’s start with the simple one. When it comes to the common operation buttons of the player, the most important one is the drag player bar.
<! -- Progress bar begin-->
<div className="audio-progress" ref={(r)= > { this.audioProgress = r }}>
<div className="audio-progress-bar" ref={(bar)= > { this.progressBar = bar }}>
<div className="audio-progress-bar-preload" style={{ width: bufferedPercent + '%' }}></div>
<div className="audio-progress-bar-current" style={{ width: `calc(${currentTime} / ${currentTotalTimeThe ${} *this.progressBar && this.progressBar.clientWidth}px) `}} ></div>
</div>
<div className="audio-progress-point-area" ref={(point)= > { this.audioPoint = point }} style={{ left: left + 'px' }}>
<div className="audio-progress-point">
</div>
</div>
</div>
<! -- Progress bar begin-->
<div className="audio-timer">
<p>{this.renderPlayTime(currentTime)}</p>
<p>{this.renderPlayTime(currentTotalTime)}</p>
</div>
<div className="audio-control">
<div className="audio-control-pre">
<img src={PreIcon} alt="" />
</div>
<div className="audio-control-play">
<img src={Play} alt="" />
</div>
<div className="audio-control-next">
<img src={NextIcon} alt="" />
</div>
</div>
Copy the code
The general principle of the progress bar is to obtain the ratio of the current playing duration and the total audio duration, and then multiply the ratio by the width of the progress bar to obtain the width of the progress bar to be filled under the current playing duration.
The “audio-progress-bar-preload” is used to make a buffer bar, and the general approach is the same. However, the buffered property of audio is used to obtain the buffer progress.
The layout code for the progress bar and play button is basically like this. In TERMS of CSS, it is important to pay attention to the hierarchical relationship between the progress bar container and the progress bar filling block and the progress bar touch points.
Player function logic
Maybe you’re wondering why you didn’t see the key audio tag. That’s because, in fact, the logical focus of the entire player is in that tag, and we need to separate it out and analyze it.
<audio
preload="metadata"
src={src}
ref={(audio)= > {this.lectureAudio = audio}}
style={{width: '1px', height: '1px', visibility: 'hidden'}}
onCanPlay={() => this.handleAudioCanplay()}
onTimeUpdate={() => this.handleTimeUpdate()}>
</audio>
Copy the code
① How to make the progress bar move?
CurrentTime is changing all the time during audio playback, so finding a constant that reflects currentTime as the length of the progress bar is key. So complicated, in fact, in the above layout code has revealed the plot,
calc(${currentTime}/${currentTotalTime} * $this.progressBar.clientWidth}px
Copy the code
In this way, it is easy to calculate the current progress needs the corresponding progress bar filling length. Again, I know how long the progress bar should be filled, but it still won’t budge… Here, we have two solutions:
- With setInterval we can check currentTime of the current audio every 300ms, and then dynamically change currentTime in state in setState, and then the component will re-render the progress bar portion of the display, thus making our progress bar move.
- Use audio’s ontimeUpdate event to dynamically calculate the width of the progress bar. This is a native event for both Audio and video. You may be able to avoid some of the pitfalls of the project. And since this is native to HTML, browser support is certainly adequate, so it should be better from a performance standpoint.
Method executed on ontimeUpdate – Reassigns currentTime each time this event is triggered, and any remaining changes can be changed by changing currentTime.
handleTimeUpdate() {
if (this.state.currentTime < (this.state.currentTotalTime - 1)) {
this.setState({
currentTime: this.lectureAudio.currentTime
});
} else{... }}Copy the code
② How to drag the progress bar on the mobile terminal?
Since it is a touch event on the mobile terminal, the touch event is naturally the protagonist. Through the touch event, we can calculate the drag distance, and then get the progress bar and the distance that the touch should move.
initListenTouch() {
this.audioPoint.addEventListener('touchstart', (e) => this.pointStart(e), false);
this.audioPoint.addEventListener('touchmove', (e) => this.pointMove(e), false);
this.audioPoint.addEventListener('touchend', (e) => this.pointEnd(e), false);
}
Copy the code
These are the three event listeners that hang to the progress bar while the component is loading. Here are the details of the three listeners.
- Touchstart — Is responsible for getting the orientation of the touch when touching the progress touch
pointStart(e) {
e.preventDefault();
let touch = e.touches[0];
this.lectureAudio.pause();
// For a better experience, I chose to pause the audio while moving the touch
this.setState({
isPlaying: false.// Play button changes
startX: touch.pageX// The x coordinate of the progress touch on the page
});
}
Copy the code
- Touchmove – Dynamically calculates the drag distance of the touch and switches to this.state.currentTime to trigger the rerendering of the component.
pointMove(e) {
e.preventDefault();
let touch = e.touches[0];
let x = touch.pageX - this.state.startX; // The sliding distance
let maxMove = this.progressBar.clientWidth;// The maximum movement distance cannot exceed the width of the progress bar
//(this.state.moveX) = this.lectureAudio.duration / this.progressBar.clientWidth;
//moveX is a fixed constant that represents the width of the progress bar relative to the total duration of the audio. We can calculate currentTime by taking the distance that the contact has moved
// The following is what happens when the contact moves, including positive movement, negative movement and limit movement at both ends.
if (x >= 0) {
if (x + this.state.startX - this.offsetWindowLeft >= maxMove) {
this.setState({
currentTime: this.state.currentTotalTime,
// Change the value of the current playback time= > {}, ()this.lectureAudio.currentTime = this.state.currentTime;
// Change the actual playback time of audio})}else {
this.setState({
currentTime: (x + this.state.startX - this.offsetWindowLeft) * this.state.moveX
}, () => {
this.lectureAudio.currentTime = this.state.currentTime; }}})else {
if (-x <= this.state.startX - this.offsetWindowLeft) {
this.setState({
currentTime: (this.state.startX + x - this.offsetWindowLeft) * this.state.moveX,
}, () => {
this.lectureAudio.currentTime = this.state.currentTime; })}else {
this.setState({
currentTime: 0= > {}, ()this.lectureAudio.currentTime = this.state.currentTime; })}}}Copy the code
- Touchend – Is responsible for restoring audio playback
pointEnd(e) {
e.preventDefault();
if (this.state.currentTime < this.state.currentTotalTime) {
this.touchendPlay = setTimeout((a)= > {
this.handleAudioPlay();
}, 300)}// As for the setTimeout of 300ms, first of all, in order to have a good experience, you can try not to have a 300ms delay when doing it, because you will find that the listening experience is not good, and the audio playback is very hasty.
// If the pause/play interval is too short, audio will fail to perform the corresponding action correctly.
}
Copy the code
③ How to realize list playing and loop playing?
Pay attention to the relationship between currentTime and Duration
handleTimeUpdate() {
if (this.state.currentTime < (this.state.currentTotalTime - 1)) {... }else {
// The audio has been played to the end
if (this.state.isLooping){// Whether to play in a loop
// Set currentTime to 0 and play()
this.setState({
currentTime: 0= > {}, ()this.lectureAudio.currentTime = this.state.currentTime;
this.lectureAudio.play(); })}else {
// To play the list, you only need to judge whether there is the next song. If there is, jump or play, if there is no, pause playing.
if (this.props.audioInfo.next_lecture_id && this.state.currentTime ! = =0) {this.handleNextLecture();
}else {
this.handleAudioPause(); }}}}Copy the code
conclusion
I don’t know if you have any feeling after watching this, as if you can’t leave currentTime in this.state.
Yes, the core of this player is currentTime, which is also intentional during development. Finally, we will find that the only variable in this component is currentTime. We can complete all requirements through the change of currentTime without considering the influence of other factors. Because all the child components are running around currentTime.
That’s my point about player development. It doesn’t have a really cool skin or really complicated features, but personally, it helps me sort out the development process of a [usable] player.