赞
踩
记录关于自己使用 Web Audio API 的 AudioContext 播放音乐的知识点。
1.列表展示音乐;
2.上/下一首、播放/暂停/续播;
3.播放模式切换:循环播放、单曲循环、随机播放;
4.播放状态显示:当前播放的音乐名、播放时间、总时间、进度条效果;
5.播放控制器显示在底部区域;
6.支持音量调节;
7.浏览器隐藏、显示的交互后,也能正常有效播放(播放、声音)。
安卓
与IOS
上有不同的兼容性,所以采用了 Web Audio API 的 AudioContext ,兼容性强大(但是截止写文章前,IOS17+版本不支持,没有声音
)。
稍微复杂点点的逻辑就是AudioContext与手机系统的关联,可以看看 AudioContext: createMediaElementSource。
test/music/musicPlayer/musics.ts
test/music/musicPlayer/useMusicPlayer.ts
test/music/index.vue
interface musicItem { title: string src: string time: string mp3Name: string } const musicList: musicItem[] = [ { title: 'How to Love', src: '', time: '03:39', mp3Name: 'sx_music_HowtoLove_CashCash' }, { title: '空空如也', src: '', time: '03:34', mp3Name: 'sx_music_kongkongruye' }, { title: '2 Soon', src: '', time: '03:19', mp3Name: 'sx_music_Soon_JonYoung' }, { title: '孤勇者', src: '', time: '04:16', mp3Name: 'sx_music_guyongzhe' }, { title: '秒针', src: '', time: '02:58', mp3Name: 'sx_music_miaozhen' }, { title: '热爱105˚的你', src: '', time: '03:15', mp3Name: 'sx_music_reai105dudeni' }, { title: '她会魔法吧', src: '', time: '03:01', mp3Name: 'sx_music_tahuimofaba' }, { title: '她会魔法吧', src: '', time: '03:01', mp3Name: 'sx_music_tahuimofaba' }, { title: '她会魔法吧', src: '', time: '03:01', mp3Name: 'sx_music_tahuimofaba' }, { title: '她会魔法吧', src: '', time: '03:01', mp3Name: 'sx_music_tahuimofaba' }, { title: '她会魔法吧', src: '', time: '03:01', mp3Name: 'sx_music_tahuimofaba' }, { title: '她会魔法吧', src: '', time: '03:01', mp3Name: 'sx_music_tahuimofaba' }, { title: '她会魔法吧', src: '', time: '03:01', mp3Name: 'sx_music_tahuimofaba' }, { title: '她会魔法吧', src: '', time: '03:01', mp3Name: 'sx_music_tahuimofaba' }, { title: '她会魔法吧', src: '', time: '03:01', mp3Name: 'sx_music_tahuimofaba' }, { title: '她会魔法吧', src: '', time: '03:01', mp3Name: 'sx_music_tahuimofaba' }, { title: '她会魔法吧', src: '', time: '03:01', mp3Name: 'sx_music_tahuimofaba' } ] // 音乐列表信息 export { type musicItem, musicList }
import { ref, nextTick } from 'vue' import { musicList } from './musics' enum PlayMode { REPEAT, // 循环播放 SINGLE_CYCLE, // 单曲循环 RANDOM // 随机播放 } const musicPlayer = ref<HTMLAudioElement | null>() const musicPlayingIndex = ref(-1) // 播放的音乐的下标 const musicIsPlaying = ref(false) // 是否播放中 const currentTime = ref(0) // 正在播放的音乐时间点 const musicPlayMode = ref(PlayMode.REPEAT) // 播放模式 const progressInterval = 500 // 计时器触发的频率 let defaultVolume = 1 // 音量 0-1 let timer: NodeJS.Timer | null = null // 计时器 ---此处需要在 .eslintrc.js/.cjs 文件中配置 globals: { NodeJS: true } let source: MediaElementAudioSourceNode | null = null let audioCtx: AudioContext | null = null let gainNode: GainNode | null = null let audioContextAttr: string | null = null if ('AudioContext' in window) { audioContextAttr = 'AudioContext' } else if ('webkitAudioContext' in window) { audioContextAttr = 'webkitAudioContext' } const useMusicPlayer = () => { const _getMusicFile = (mp3Name: string) => { // 此处需要相对路径 // vite项目 // return new URL(`../../../assets/music/${mp3Name}.mp3`, import.meta.url).href // webpack项目 return require(`../../../assets/music/${mp3Name}.mp3`) } /** 设置:音量百分比 0-100 变为 0-1 * @param v number 0-100 */ const _saveDefaultVolume = (v: number) => { let num = v if (v < 0) { num = 0 } else if (v > 100) { num = 100 } defaultVolume = num / 100 return defaultVolume } /** * 计时器:回调-更新显示-MP3的播放时间 */ const _intervalUpdatePlayTime = () => { const player = musicPlayer.value if (!player) return currentTime.value = player.currentTime } /** * 计时器:清除 */ const _clearTimer = () => { if (!timer) return clearInterval(timer) timer = null } /** * 计时器:绑定&开始 */ const _startTimer = () => { _clearTimer() timer = setInterval(_intervalUpdatePlayTime, progressInterval) } /** * 方法:取两个值之间的随机数 */ const _random = (min = 0, max = 100) => Math.floor(Math.random() * (max - min + 1)) + min /** * 销毁:断开audio与AudioContext之间的链接 */ const _destroyConnect = () => { if (source) { source.disconnect() } if (gainNode) { gainNode.disconnect() } if (audioCtx) { audioCtx.close() } source = null gainNode = null audioCtx = null musicPlayer.value = null } /** * 音乐:初始化audio与AudioContext的绑定 * 目的是为了 IOS 上能调整音量 */ const _init = () => { if (!audioContextAttr) return // 先暂停已有的播放 pause() // 对已创建的绑定关系进行解绑 _destroyConnect() // 若在body中找得到对应的dom,则进行移除 const findDom = document.getElementById('musicPlayerAudio') as HTMLAudioElement if (findDom) { findDom.remove() } // 创建audio,加入body中 const dom = document.createElement('audio') dom.id = 'musicPlayerAudio' document.body.appendChild(dom) // 给audio绑定播放结束的回调函数 dom.onended = onAudioEnded // 创建AudioContext、source、gainNode,进行关联(便于IOS控制音量) const UseAudioContext = (window as any)[audioContextAttr] audioCtx = new UseAudioContext() if (!audioCtx) return source = audioCtx.createMediaElementSource(dom) gainNode = audioCtx.createGain() source.connect(gainNode) gainNode.connect(audioCtx.destination) // 设置音量 if (defaultVolume === 0) { dom.muted = true } else { dom.muted = false } gainNode.gain.value = defaultVolume // 存储dom,便于后续访问audio对应的属性 musicPlayer.value = dom // 若播放控制器的状态未启动,则启动 if (audioCtx && audioCtx.state === 'suspended') { audioCtx.resume() } } /** * 音乐:播放器-音量调整 */ const setVolume = (volume: number) => { const v = _saveDefaultVolume(volume) const player = musicPlayer.value if (!player) return if (v === 0) { player.muted = true } else { player.muted = false } if (!gainNode || !gainNode.gain) return gainNode.gain.value = v } /** * 音乐:播放器-暂停 */ const pause = () => { const player = musicPlayer.value if (!musicIsPlaying.value || !player) { return } musicIsPlaying.value = false player.pause() _clearTimer() } /** * 音乐:播放器-播放 */ const playByLast = () => { const player = musicPlayer.value if (!player || !player.src) return if (audioCtx && audioCtx.state === 'suspended') { audioCtx.resume() } nextTick(() => { // play触发时,会先自动加载资源 player.play().then(() => { musicIsPlaying.value = true _startTimer() }) }) } /** * 音乐:播放器-播放-通过下标 */ const playByIndex = (index: number) => { if (index < 0 || index + 1 > musicList.length) { return } musicIsPlaying.value = false // 重新初始化,便于释放上一个播放器所占用的内存 _init() const player = musicPlayer.value if (!player) { return } // 重置当前播放了的时长 currentTime.value = 0 // 更新要播放的下标 musicPlayingIndex.value = index if (!musicList[index].src) { // 若资源路径不存在,则进行对应的路径引入 musicList[index].src = _getMusicFile(musicList[index].mp3Name) } if (!musicList[index].src) { console.error('find music file failed') return } player.src = musicList[index].src playByLast() } /** * 音乐:随机播放 */ const randomPlay = () => { const index = _random(0, musicList.length - 1) playByIndex(index) } /** * 音乐:播放器-下一首 */ const playNext = () => { if (musicPlayMode.value === PlayMode.RANDOM) { randomPlay() } else { const index: number = musicPlayingIndex.value + 1 === musicList.length ? 0 : musicPlayingIndex.value + 1 playByIndex(index) } } /** * 音乐:播放器-上一首 */ const playPrev = () => { if (musicPlayMode.value === PlayMode.RANDOM) { randomPlay() } else { const index: number = musicPlayingIndex.value < 1 ? musicList.length - 1 : musicPlayingIndex.value - 1 playByIndex(index) } } /** * 回调:播放结束后,下一首播放什么 */ const onAudioEnded = () => { switch (musicPlayMode.value) { case PlayMode.REPEAT: playNext() break case PlayMode.SINGLE_CYCLE: playByIndex(musicPlayingIndex.value) break case PlayMode.RANDOM: randomPlay() break default: break } return true } /** 自动播放音乐 */ const startPlayInRoom = () => { // 用户第一次点击时,自动播放音乐 const initMusicAutoPlayOnReload = () => { document.removeEventListener('click', initMusicAutoPlayOnReload, true) playByIndex(0) } document.addEventListener('click', initMusicAutoPlayOnReload, true) } return { musicList, musicPlayer, musicPlayingIndex, musicIsPlaying, currentTime, musicPlayMode, setVolume, pause, playByIndex, playByLast, playPrev, playNext, _clearTimer, startPlayInRoom } } export { PlayMode, useMusicPlayer }
<template> <div class="music-box"> <!-- 音乐列表 --> <div class="music-list"> <div v-for="(music, index) in musicList" :key="index" class="music-item" :class="{ 'music-item-active': musicPlayer.musicPlayingIndex.value === index }" @click.stop="switchAudio(index)" > <div class="item-left"> <div class="item-left-title"> {{ music.title }} </div> <svg v-if="musicPlayer.musicPlayingIndex.value === index && musicPlayer.musicIsPlaying.value" id="equalizer" width="13px" height="11px" viewBox="0 0 10 7" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" > <g fill="#3994f9"> <rect id="bar1" transform="translate(0.500000, 6.000000) rotate(180.000000) translate(-0.500000, -6.000000) " x="0" y="5" width="1" height="2px" ></rect> <rect id="bar2" transform="translate(3.500000, 4.500000) rotate(180.000000) translate(-3.500000, -4.500000) " x="3" y="2" width="1" height="5" ></rect> <rect id="bar3" transform="translate(6.500000, 3.500000) rotate(180.000000) translate(-6.500000, -3.500000) " x="6" y="0" width="1" height="7" ></rect> </g> </svg> <svg v-else-if="musicPlayer.musicPlayingIndex.value === index" id="equalizer" width="13px" height="11px" viewBox="0 0 10 7" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" > <g fill="#3994f9"> <rect x="0" y="5" width="1" height="2px"></rect> <rect x="3" y="2" width="1" height="5"></rect> <rect x="6" y="0" width="1" height="7"></rect> </g> </svg> </div> <div class="item-right"> {{ music.time }} </div> </div> </div> <!-- 播放控制 --> <div class="music-control"> <div class="control-content"> <div class="control-content-left"> <div class="music-btn prev" @touchstart.passive="onTouchEvent" @touchend.passive="onTouchEvent" @click="prev" /> <div :class="['music-btn', musicPlayer.musicIsPlaying.value ? 'pause' : 'play']" @touchstart.passive="onTouchEvent" @touchend.passive="onTouchEvent" @click="togglePlayer" /> <div class="music-btn next" @touchstart.passive="onTouchEvent" @touchend.passive="onTouchEvent" @click="next" /> </div> <div class="control-content-center"> <div class="center-title"> {{ currentMusicTitle || '-' }} </div> <div ref="audioProgressWrap" class="center-progress-wrap"> <div ref="audioProgress" class="center-progress-wrap-active" /> </div> <div class="center-time"> <div class="center-time-now"> {{ formatSecond(musicPlayer.currentTime.value) }} </div> <div class="center-time-total"> {{ currentMusicTotalTimeStr }} </div> </div> </div> <div class="control-content-right"> <div v-if="musicPlayer.musicPlayMode.value === PlayMode.REPEAT" class="music-btn playRepeat" @touchstart.passive="onTouchEvent" @touchend.passive="onTouchEvent" @click="nextPlayMode" /> <div v-if="musicPlayer.musicPlayMode.value === PlayMode.SINGLE_CYCLE" class="music-btn singleCycle" @touchstart.passive="onTouchEvent" @touchend.passive="onTouchEvent" @click="nextPlayMode" /> <div v-if="musicPlayer.musicPlayMode.value === PlayMode.RANDOM" class="music-btn playRandom" @touchstart.passive="onTouchEvent" @touchend.passive="onTouchEvent" @click="nextPlayMode" /> </div> </div> </div> </div> </template> <script setup lang="ts"> import { ref, computed, watch } from 'vue' import { PlayMode, useMusicPlayer } from './musicPlayer/useMusicPlayer' import { musicList } from './musicPlayer/musics' const systemSoundMode = ref(true) // 该变量应该在store中,便于设置页面控制全局声音的开启与否 const musicPlayer = useMusicPlayer() const audioProgressWrap = ref() const audioProgress = ref() /** 当前播放的音乐名 */ const currentMusicTitle = computed(() => musicPlayer.musicPlayingIndex.value + 1 > 0 ? musicList[musicPlayer.musicPlayingIndex.value].title : '' ) /** 当前播放的音乐总时间 */ const currentMusicTotalTimeStr = computed(() => musicPlayer.musicPlayingIndex.value + 1 > 0 ? musicList[musicPlayer.musicPlayingIndex.value].time : '00:00' ) /** 操作:切换播放模式 */ const nextPlayMode = () => { musicPlayer.musicPlayMode.value = (musicPlayer.musicPlayMode.value + 1) % 3 switch (musicPlayer.musicPlayMode.value) { case PlayMode.REPEAT: console.log('循环播放') break case PlayMode.RANDOM: console.log('随机播放') break case PlayMode.SINGLE_CYCLE: console.log('单曲循环') break default: break } } /** 事件:当点击按钮时的过渡效果 */ const onTouchEvent = (event: Event) => { const tg = event.currentTarget as HTMLElement if (!tg) return if (event.type === 'touchstart') { tg.classList.add('touch') } if (event.type === 'touchend') { tg.classList.remove('touch') } } /** 格式化:秒数=>ss:mm */ const formatSecond = (second: number) => { let hourStr = `${Math.floor(second / 60)}` let secondStr = `${Math.ceil(second % 60)}` if (hourStr.length === 1) { hourStr = `0${hourStr}` } if (secondStr.length === 1) { secondStr = `0${secondStr}` } return `${hourStr}:${secondStr}` } /** 操作:播放所选音乐 */ const switchAudio = (index: number) => { const player = musicPlayer.musicPlayer.value if (!systemSoundMode.value) { window.alert('所有声音已关闭') } if (player?.src && player?.src.includes(musicList[index].mp3Name)) { if (musicPlayer.musicIsPlaying.value) { return } musicPlayer.playByLast() } else { musicPlayer.playByIndex(index) } } /** 操作:上一首 */ const prev = () => { if (!systemSoundMode.value) { window.alert('所有声音已关闭') } musicPlayer.playPrev() } /** 操作:下一首 */ const next = () => { if (!systemSoundMode.value) { window.alert('所有声音已关闭') } musicPlayer.playNext() } /** 操作:播放/暂停 */ const togglePlayer = () => { const player = musicPlayer.musicPlayer.value if (!systemSoundMode.value) { window.alert('所有声音已关闭') } if (musicPlayer.musicIsPlaying.value && player?.src) { // 正在播放,则暂停 musicPlayer.pause() } else if (!player?.src) { // 未开始播放,则播放第一首 musicPlayer.playByIndex(0) } else { // 暂停了,则继续播放刚才的 musicPlayer.playByLast() } } /** 监听:当前播放中的音乐的进度时间=>进度条变化 */ watch( () => musicPlayer.currentTime.value, () => { const player = musicPlayer.musicPlayer.value if (!audioProgressWrap.value || !audioProgress.value || !player) { return } const offsetLeft = (player.currentTime / player.duration) * audioProgressWrap.value.offsetWidth audioProgress.value.style.width = `${offsetLeft}px` } ) </script> <style lang="less" scoped> @bottomHeight: 97px; @controlHeight: 63px; @controlBottom: 34px; .music-box { width: 100%; height: 100%; background-color: #141624; position: relative; display: flex; flex-direction: column; } .music-list { flex: 1; overflow-y: auto; scrollbar-width: none; -ms-overflow-style: none; &::-webkit-scrollbar { display: none; } .music-item:nth-of-type(1) { margin-top: 7px; } .music-item { padding: 12px 20px 19px; display: flex; align-items: center; justify-content: space-between; font-size: 14px; font-weight: 500; line-height: 120%; color: #8f9095; .item-left { display: flex; align-items: center; .item-left-title { height: 17px; margin-right: 10px; } #equalizer { position: relative; } #bar1 { animation: bar1 1.2s infinite linear; } #bar2 { animation: bar2 0.8s infinite linear; } #bar3 { animation: bar3 1s infinite linear; } #bar4 { animation: bar4 0.7s infinite linear; } @keyframes bar1 { 0% { height: 2px; } 50% { height: 7px; } 100% { height: 2px; } } @keyframes bar2 { 0% { height: 5px; } 40% { height: 1px; } 80% { height: 7px; } 100% { height: 5px; } } @keyframes bar3 { 0% { height: 7px; } 50% { height: 0; } 100% { height: 7px; } } @keyframes bar4 { 0% { height: 2px; } 50% { height: 7px; } 100% { height: 2px; } } } } .music-item-active { .item-left { .item-left-title { color: #3994f9; } } .item-right { color: #3994f9; } } } .music-control { height: @bottomHeight; padding: 0 10px; background-color: #141624; .control-content { height: @controlHeight; border-radius: 7px; background-color: #1b1d2a; display: flex; align-items: center; justify-content: space-between; .control-content-left { display: flex; align-items: center; .prev, .pause, .play { margin-right: 10px; } .next { margin-right: 17px; } } .control-content-center { margin-top: 1px; flex: 1; .center-title { margin-bottom: 5px; line-height: 120%; font-size: 13px; font-weight: 500; color: #fff; } .center-progress-wrap { width: 100%; height: 2px; background-color: #3e404e; .center-progress-wrap-active { width: 0; height: 100%; background-color: #3994f9; } } .center-time { height: 50%; margin-top: 10px; display: flex; justify-content: space-between; align-items: center; .center-time-now, .center-time-total { font-size: 10px; font-weight: 400; line-height: 12px; color: #3994f9; } .center-time-total { color: #8f9095; } } } .control-content-right { padding-left: 10px; } .music-btn { width: 33px; height: 33px; &.prev { background: url('../../assets/images/music/music-prev.png'); background-size: 100%; background-repeat: no-repeat; &.touch { background: url('../../assets/images/music/music-prev-touch.png'); } } &.play { background: url('../../assets/images/music/music-play.png'); background-size: 100%; background-repeat: no-repeat; &.touch { background: url('../../assets/images/music/music-play-touch.png'); } } &.pause { background: url('../../assets/images/music/music-pause.png'); background-size: 100%; background-repeat: no-repeat; &.touch { background: url('../../assets/images/music/music-pause-touch.png'); } } &.next { background: url('../../assets/images/music/music-next.png'); background-size: 100%; background-repeat: no-repeat; &.touch { background: url('../../assets/images/music/music-next-touch.png'); } } &.playRepeat { background: url('../../assets/images/music/music-repeat.png'); background-size: 100%; background-repeat: no-repeat; &.touch { background: url('../../assets/images/music/music-repeat-touch.png'); } } &.singleCycle { background: url('../../assets/images/music/music-single-cycle.png'); background-size: 100%; background-repeat: no-repeat; &.touch { background: url('../../assets/images/music/music-single-cycle-touch.png'); } } &.playRandom { background: url('../../assets/images/music/music-random.png'); background-size: 100%; background-repeat: no-repeat; &.touch { background: url('../../assets/images/music/music-random-touch.png'); } } } } } </style>
觉得有用的朋友请用你的金手指点一下赞,或者评论留言一起探讨技术!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。