共计 6201 个字符,预计需要花费 16 分钟才能阅读完成。
要写一个简略的音频或者视频播放器的进度条还是要思考不少货色的,看看借助于 VueUse
来实现能有多省事~
首先确定播放器进度条的性能
- 有播放暂停按钮
- 进度条能够追随播放丝滑更新
- 有以后播放工夫和总工夫能够依据播放更新以后工夫
- 能够点击进度条的某一处跳转到指定处进行播放
咱们先简略梳理这四个性能,咱们的重心是在这个进度条的渲染和交互上。
嫌子弹飞慢的,间接看最初上任鹅城那段(doge)
依据既定的性能来确定咱们的构造
要有风🌬️,要有肉🥩
要有一个能够蕴含理论进度条的壳子(风),再来一个另外一种色彩理论进度条的条子(肉),前面再加一个小圆点示意以后播放到哪了。
<div class="process-bar" rounded-8px h-8px flex-1 bg-gray-200 cursor-pointer flex items-center> | |
<div h-full bg-purple-400 rounded-8px :style="{width: `${playTime / duration * 100}%` }" /> | |
<div w-10px h-10px rounded-full bg-purple-500 ml--6px shadow hover:w-12px hover:h-12px /> | |
</div> |
要有火锅🍲,要有雾🌁
这里再来一个 audio
的资源(火锅),而后来个封面(雾)
<audio ref="audioRef" src="https://rust-fe-shared.pages.dev/The%20Sun%20Also%20Rises.mp3" @play="onAudioPlay" @loadedmetadata="onLoadedmetadata" @pause="onAudioPause" @ended="onAudioEnded" /> | |
<div class="preview-audio-cover" :class="`${playing ?'motion-safe:animate-spin':''}`"> | |
<img w-full src="https://rust-fe-shared.pages.dev/record.png" alt=""> | |
<img class="audio-cover" src="https://rust-fe-shared.pages.dev/cover.webp" alt=""> | |
</div> |
要有美女👩,要有驴🫏
要有一个播放暂停按钮吧(美女),要展现以后工夫和总工夫吧(驴)
正经备注,须要用到图标库:
@iconify-json/carbon
配合UnoCSS
来应用。
<div class="play-btn" @click="playOrPause"> | |
<div v-if="playing" m-auto i-carbon:pause-filled /> | |
<div v-else m-auto i-carbon:play-filled-alt /> | |
</div> | |
<p tabular-nums> | |
{{fmtTime(playTime) }} / {{fmtTime(duration) }} | |
</p> |
起来起来,一起吃一起唱
让咱们开始这些实现性能吧
资源加载实现更新总时长
在资源元数据加载实现之后更新总时长
function onLoadedmetadata() {duration.value = audioRef.value.duration}
借助 dayjs
解决工夫展现
function fmtTime(duration: number) {const hours = Boolean(Math.floor(duration / 3600)) | |
return dayjs(duration * 1000) | |
.subtract(8, 'hour') | |
.format(`${hours ? 'HH:' : ''}mm:ss`) | |
} |
进度条、以后工夫更新
应用 VueUse
提供的 useRafFn 和 useFps 配合来解决播放过程中进度条的更新:
useFafFn
是一个便携应用 requestAnimationFrame API
来更加顺畅更新动画的办法,会返回两个办法:resume、pause
和一个状态 isActive
这次咱们只须要用到两个管制的办法。pause
用来管制暂停以后的逻辑执行,resume
是继续执行每帧距离内的逻辑。
useFps
是为了获取以后显示设施刷新率的办法,用来适配不同设施(次要是为适配高刷新率屏幕)更新画面的频率。
当资源开始播放的时候每更新一帧就更新一次以后播放工夫 playTime
的值(上面代码第 6 行开始),进度条中以后进度的展现逻辑就是依据 playTime / duration * 100%
所计算出来的百分比,所以主动会更新款式,以后播放时长的更新也会随着 {{fmtTime(playTime) }} / {{fmtTime(duration) }}
这段逻辑来更新,所以咱们的进度条和以后播放工夫的丝滑更新就这么实现了,当资源被暂停或者播放完结就完结更新。
有同学会说了我用 timeupdat 事件更新不简略吗?当然能够啊,然而这个事件触发的频率太低了:每秒触发 4-66 次,触发频率由零碎决定(文档),所以如果用这个来做,当资源市场比拟短的时候你会看到进度条的画面卡顿式更新,体验及其不敌对。
// 拿到以后的 fps | |
const fps = useFps() | |
// 在播放中中每帧都要更新以后播放到的时长 | |
// 这样进度条更新就更加顺畅 | |
const {resume, pause} = useRafFn(() => { | |
playTime.value += 1 / fps.value | |
// 如果以后工夫超出了音频总时长,就完结 | |
if (playTime.value > duration.value) | |
onAudioEnded()}) | |
// 初始要暂停执行帧距离配置的逻辑 | |
pause() | |
// 音频播放完结要暂停 | |
function onAudioEnded() {onAudioPause() | |
} | |
// 暂停的时候也要把暂停 | |
function onAudioPause() { | |
playing.value = false | |
pause()} | |
// 当资源开始播放的时候就开始执行每帧进度条更新 | |
function onAudioPlay() { | |
playing.value = true | |
resume()} |
点击进度条的某一处跳转指定地位
咱们这里须要依据点击的地位在进度条上所占的百分位来按比例跳转到总时长的百分位,说白了咱们须要获取到点击地位在进度条上所占的百分比,useMouseInElement 能够祝咱们一臂之力,这个办法提供了以后鼠标在指定 DOM
元素上的信息,咱们只须要两个信息 elementX, elementWidth
第一个是以后鼠标在元素上的 X
坐标,第二个是以后元素的宽度,完满~
// 进度条元素的 ref | |
const processRef = ref() | |
// 音频元素的 ref | |
const audioRef = ref() | |
// 音频的时长 | |
const duration = ref(0) | |
const {elementX, elementWidth} = useMouseInElement(processRef) | |
function seekAudio() { | |
// 依据百分比计算出要跳转的工夫 | |
const currentTime = elementX.value / elementWidth.value * duration.value | |
// 跳转到以后鼠标点击地位的百分位工夫 | |
audioRef.value.currentTime = currentTime | |
playTime.value = currentTime | |
} |
解决播放暂停和状态展现
这部分就简略多了
// 播放或者暂停 | |
function playOrPause() {if (playing.value) {audioRef.value.pause() | |
} | |
else {if (audioRef.value.ended) | |
playTime.value = 0 | |
audioRef.value.play()} | |
} |
让代码飞一会
应用了 UnoCSS
还用了 @iconify-json/carbon
的图标,记得装置 @vueuse/core, dayjs,@iconify-json/carbon
,所有的代码:
<script setup lang="ts"> | |
import dayjs from 'dayjs' | |
const processRef = ref() | |
const audioRef = ref() | |
const playTime = ref(0) | |
const duration = ref(0) | |
const playing = ref(false) | |
const fps = useFps() | |
const {elementX, elementWidth} = useMouseInElement(processRef) | |
const {resume, pause} = useRafFn(() => { | |
playTime.value += 1 / fps.value | |
if (playTime.value > duration.value) | |
onAudioEnded()}) | |
pause() | |
function onLoadedmetadata() {duration.value = audioRef.value.duration} | |
function playOrPause() {if (playing.value) {audioRef.value.pause() | |
} | |
else {if (audioRef.value.ended) | |
playTime.value = 0 | |
audioRef.value.play()} | |
} | |
function onAudioEnded() {onAudioPause() | |
} | |
function onAudioPause() { | |
playing.value = false | |
pause()} | |
function onAudioPlay() { | |
playing.value = true | |
resume()} | |
function seekAudio() { | |
const currentTime = elementX.value / elementWidth.value * duration.value | |
audioRef.value.currentTime = currentTime | |
playTime.value = currentTime | |
} | |
function fmtTime(duration: number) {const hours = Boolean(Math.floor(duration / 3600)) | |
return dayjs(duration * 1000) | |
.subtract(8, 'hour') | |
.format(`${hours ? 'HH:' : ''}mm:ss`) | |
} | |
</script> | |
<template> | |
<div class="preview-audio"> | |
<div class="preview-audio-cover" :class="`${playing ?'motion-safe:animate-spin':''}`"> | |
<img w-full src="https://rust-fe-shared.pages.dev/record.png" alt=""> | |
<img class="audio-cover" src="https://rust-fe-shared.pages.dev/cover.webp" alt=""> | |
</div> | |
<div class="play-btn" @click="playOrPause"> | |
<div v-if="playing" m-auto i-carbon:pause-filled /> | |
<div v-else m-auto i-carbon:play-filled-alt /> | |
</div> | |
<div class="audio-process"> | |
<div ref="processRef" class="process-bar" @click="seekAudio"> | |
<div h-full bg-purple-400 w-full rounded-8px :style="{width: `${playTime / duration * 100}%` }" /> | |
<div w-10px h-10px rounded-full bg-purple-500 ml--6px shadow hover:w-12px hover:h-12px /> | |
</div> | |
<p tabular-nums> | |
{{fmtTime(playTime) }} / {{fmtTime(duration) }} | |
</p> | |
</div> | |
<audio ref="audioRef" src="https://rust-fe-shared.pages.dev/The%20Sun%20Also%20Rises.mp3" @play="onAudioPlay" @loadedmetadata="onLoadedmetadata" @pause="onAudioPause" @ended="onAudioEnded" /> | |
</div> | |
</template> | |
<style scoped> | |
.preview-audio {--at-apply: p-3 flex flex-col items-center;} | |
.preview-audio-cover {--at-apply: relative w-180px h-180px;} | |
.audio-cover {--at-apply: absolute rounded-full left-0 top-0 w-120px h-120px m-30px;} | |
.play-btn {--at-apply: my-24px bg-purple-400 w-64px h-62px text-white text-3xl flex rounded-full cursor-pointer;} | |
.audio-process {--at-apply: flex items-center gap-12px w-full;} | |
.process-bar {--at-apply: rounded-8px h-8px flex-1 bg-gray-200 cursor-pointer flex items-center;} | |
</style> |
当然你也能够在这里找到全副的我的项目代码,在这里看到最终的成果。
总结
应用 VueUse
能够简化很多咱们应用 Web API
的代码逻辑,咱们这次应用 useMouseInElement, useRafFn, useFps
就能够实现丝滑更新播放器进度条的性能,省去了很多额定的代码和边界状况思考,VueUse
中还有很多不错的办法,咱们之后会一一介绍。有用请点赞,喜爱请关注,我是 Senar
,咱们下一篇再见~
参考链接:
vueuse-player-process https://vueuse-process.pages.dev/
vueuse-player-process-git https://github.com/luvletterldl/vueuse-process
vueuse-useRafFn https://vueuse.org/core/useRafFn/#useraffn
vueuse-useMouseInElement https://vueuse.org/core/useMouseInElement/#usemouseinelement
vueuse-useFps https://vueuse.org/core/useFps/#usefps