I am working on an audio player with Vue 3 and the Napster API.
Project details
The player has a progress bar. I use the trackProgress
computed property to update the progress in real-time:
<div >
<span :style="{ width: trackProgress '%' }"></span>
</div>
const musicApp = {
data() {
return {
player: new Audio(),
trackCount: 0,
tracks: [],
muted: false,
isPlaying: false
};
},
methods: {
async getTracks() {
try {
const response = await axios
.get(
"https://api.napster.com/v2.1/tracks/top?apikey=ZTk2YjY4MjMtMDAzYy00MTg4LWE2MjYtZDIzNjJmMmM0YTdm"
)
.catch((error) => {
console.log(error);
});
this.tracks = response;
this.tracks = response.data.tracks;
} catch (error) {
console.log(error);
}
},
nextTrack() {
if (this.trackCount < this.tracks.length - 1) {
this.trackCount ;
this.setPlayerSource();
this.playPause();
}
},
prevTrack() {
if (this.trackCount >= 1) {
this.trackCount--;
this.setPlayerSource();
this.playPause();
}
},
setPlayerSource() {
this.player.src = this.tracks[this.trackCount].previewURL;
},
playPause() {
if (this.player.paused) {
this.isPlaying = true;
this.player.play();
} else {
this.isPlaying = false;
this.player.pause();
}
},
toggleMute() {
this.player.muted = !this.player.muted;
this.muted = this.player.muted;
}
},
async created() {
await this.getTracks();
this.setPlayerSource();
this.player.addEventListener("ended", () => {
this.isPlaying = false;
});
},
computed: {
trackProgress() {
this.player.addEventListener("loadedmetadata", () => {
return (this.player.currentTime / this.player.duration) * 100;
});
}
}
};
Vue.createApp(musicApp).mount("#audioPlayer");
html,
body {
margin: 0;
padding: 0;
font-size: 16px;
}
body * {
box-sizing: border-box;
font-family: "Montserrat", sans-serif;
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.player-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #2998ff;
background-image: linear-gradient(62deg, #2998ff 0%, #5966eb 100%);
}
#audioPlayer {
width: 300px;
height: 300px;
border-radius: 8px;
position: relative;
overflow: hidden;
background-color: #00ca81;
background-image: linear-gradient(160deg, #00ca81 0%, #ffffff 100%);
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
display: flex;
flex-direction: column;
align-items: center;
}
.volume {
color: #ff0057;
opacity: 0.9;
display: inline-block;
width: 20px;
font-size: 20px;
position: absolute;
top: 5px;
right: 6px;
cursor: pointer;
}
.album {
width: 100%;
flex: 1;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
.album-items {
padding: 0 10px;
text-align: center;
}
.cover {
width: 150px;
height: 150px;
margin: auto;
box-shadow: 0px 5px 12px 0px rgba(0, 0, 0, 0.17);
border-radius: 50%;
background: url("https://w7.pngwing.com/pngs/710/955/png-transparent-vinyl-record-artwork-phonograph-record-compact-disc-lp-record-disc-jockey-symbol-miscellaneous-classical-music-sound.png") center top transparent;
background-size: cover;
}
.cover.spinning {
webkit-animation: spin 6s linear infinite;
/* Safari */
animation: spin 6s linear infinite;
}
.info {
width: 100%;
padding-top: 5px;
color: #000;
opacity: 0.85;
}
.info h1 {
font-size: 11px;
margin: 5px 0 0 0;
}
.info h2 {
font-size: 10px;
margin: 3px 0 0 0;
}
.to-bottom {
width: 100%;
margin-top: auto;
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.progress-bar {
background-color: #ff0057;
opacity: 0.9;
height: 3px;
width: 100%;
}
.progress-bar span {
display: block;
height: 3px;
width: 0;
background: rgba(255, 255, 255, 0.4);
}
.controls {
width: 150px;
display: flex;
height: 60px;
justify-content: space-between;
align-items: center;
}
.controls .navigate {
display: flex;
box-shadow: 1px 2px 7px 2px rgba(0, 0, 0, 0.09);
width: 33px;
height: 33px;
line-height: 1;
color: #ff0057;
cursor: pointer;
background: #fff;
opacity: 0.9;
border-radius: 50%;
text-align: center;
justify-content: center;
align-items: center;
}
.controls .navigate.disabled {
pointer-events: none;
color: #606060;
background-color: #f7f7f7;
}
.controls .navigate.navigate-play {
width: 38px;
height: 38px;
}
.navigate-play .fa-play {
margin-left: 3px;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<script src="https://unpkg.com/[email protected]/dist/axios.min.js"></script>
<script src="https://unpkg.com/vue@next"></script>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;500&display=swap" rel="stylesheet">
<div >
<div id="audioPlayer">
<span @click="toggleMute">
<i v-show="!muted" ></i>
<i v-show="muted" ></i>
</span>
<div >
<div >
<div :></div>
<div >
<h1>{{tracks[trackCount].name}}</h1>
<h2>{{tracks[trackCount].artistName}}</h2>
</div>
</div>
</div>
<div >
<div >
<span :style="{ width: trackProgress '%' }"></span>
</div>
<div >
<div : @click="prevTrack">
<i ></i>
</div>
<div @click="playPause">
<i v-show="!isPlaying" ></i>
<i v-show="isPlaying" ></i>
</div>
<div : @click="nextTrack">
<i ></i>
</div>
</div>
</div>
</div>
</div>
The problem
For a reason I have not been able to figure out, the style is not binded to the span element inside progress-bar
.
What have I missed?
UPDATE
Using setInterval
inside the created hook, works, but I would rather avoid it?
this.player.addEventListener("loadedmetadata", () => {
setInterval(() => {
this.trackProgress =
(this.player.currentTime / this.player.duration) * 100;
}, 100);
});
What's a better alternative?
CodePudding user response:
Description of the solution process:
- setup a listener for the player's
timeupdate
event - update the component's
data
whentimeupdate
fires - use a
percentageProgress
computed property to calculate the progress and use this in the template. (You could still use yourtrackProgress
property butpercentageProgress
is a bit clearer semantically.)
Implementation:
data() {
return {
player: new Audio(),
trackCount: 0,
tracks: [],
muted: false,
isPlaying: false,
currentTime: 0
};
},
computed: {
percentageProgress() {
return (this.currentTime / this.player.duration) * 100;
}
}
created() {
this.player.addEventListener("timeupdate", () => {
this.currentTime = this.player.currentTime;
});
}
On another note, computed getters must return a value. Your computed property doesn't return anything.
CodePudding user response:
You should create a data property trackProgress and update it in the listener which you create in created() hook (similar to ended event).
CodePudding user response:
You keep adding event listeners in your computed property, which makes no sense at all.
Also these listeners return the value you want, but these just go into the digital nirvana because noone can take the return value when the event occurs and do something with it.
Instead of a computed property, add a trackProgress
property in your data()
model, and move the adding of the event listener to the mounted
part, so it's only added once.
That listener will then update your model property trackProgress
.
data() { return {
player: new Audio(),
trackCount: 0,
tracks: [],
muted: false,
isPlaying: false,
trackProgress: 0,
} }
and
mounted: function () {
this.player.addEventListener("timeupdate", () => {
this.trackProgress = (this.player.currentTime / this.player.duration) * 100;
});
}
CodePudding user response:
You clearly don't know what you doing. Let me try to explain.
Why first solution was wrong:
Computed property is synchronous thing. So when it is called it allways expect to return a value right away. That's not what is happening in your code because this part of code
() => {
return (this.player.currentTime / this.player.duration) * 100;
}
is called when player sends event loadedmetadata
. And that will always happen after some time. You therefore allways returned nothing when this computed was called.
Why Second (UPDATE) solution is wrong
When you add event listener in interval, it's gonna register new listener every 100ms, so after a while there will be thounsands, milions, infinite needless changes on this.trackProgress
and your machine will burst in flames. You need only one listener. What were you looking for was probably setTimeout
instead of setInterval
. But even this is wrong solution I guess. I think the reason why this isn't working without some wait time is because this.player
doesn't exist yet. Try use mounted()
hook instead. DOM elements should be ready in mounted hook.