feat: 优化播放 修改为howler 修复搜索导致播放无限卡顿问题(#15)

- 优化了整个项目的播放
- 去除audio
- 优化歌词页 歌词同步时间

fixes #15
This commit is contained in:
alger
2024-12-12 22:18:52 +08:00
parent bb99049991
commit cebf313075
12 changed files with 199 additions and 182 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# 你的接口地址 (必填) # 你的接口地址 (必填)
VITE_API = *** VITE_API_LOCAL = ***
# 音乐破解接口地址 # 音乐破解接口地址
VITE_API_MUSIC = *** VITE_API_MUSIC = ***
# 代理地址 # 代理地址
+9 -11
View File
@@ -68,23 +68,21 @@
```bash ```bash
# .env.development # .env.development
# 你的接口地址 (必填) VITE_API_LOCAL = /api
VITE_API = ***
# 音乐破解接口地址
VITE_API_MUSIC = ***
# 代理地址
VITE_API_PROXY = ***
# 本地运行代理地址
VITE_API_PROXY = /api
VITE_API_MUSIC_PROXY = /music VITE_API_MUSIC_PROXY = /music
VITE_API_PROXY_MUSIC = /music_proxy VITE_API_PROXY_MUSIC = /music_proxy
# 你的接口地址 (必填)
VITE_API = ***
# 音乐po接口地址
VITE_API_MUSIC = ***
VITE_API_PROXY = ***
# .env.production # .env.production
# 你的接口地址 (必填) # 你的接口地址 (必填)
VITE_API = *** VITE_API = ***
# 音乐破解接口地址 # 音乐po接口地址
VITE_API_MUSIC = *** VITE_API_MUSIC = ***
# 代理地址 # 代理地址
VITE_API_PROXY = *** VITE_API_PROXY = ***
+3 -1
View File
@@ -17,7 +17,9 @@
"b:win": "cross-env NODE_ENV=production npm run build && npm run b:win:x64 && npm run b:win:x86 && npm run b:win:arm" "b:win": "cross-env NODE_ENV=production npm run build && npm run b:win:x64 && npm run b:win:x86 && npm run b:win:arm"
}, },
"dependencies": { "dependencies": {
"electron-store": "^8.1.0" "@types/howler": "^2.2.12",
"electron-store": "^8.1.0",
"howler": "^2.2.4"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss7-compat": "^2.2.4", "@tailwindcss/postcss7-compat": "^2.2.4",
+1 -5
View File
@@ -1,6 +1,5 @@
<template> <template>
<div class="app-container" :class="{ mobile: isMobile }"> <div class="app-container" :class="{ mobile: isMobile }">
<audio id="MusicAudio" ref="audioRef" :src="playMusicUrl" :autoplay="play"></audio>
<n-config-provider :theme="darkTheme"> <n-config-provider :theme="darkTheme">
<n-dialog-provider> <n-dialog-provider>
<router-view></router-view> <router-view></router-view>
@@ -11,15 +10,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { darkTheme } from 'naive-ui'; import { darkTheme } from 'naive-ui';
import { computed, onMounted } from 'vue'; import { onMounted } from 'vue';
import store from '@/store'; import store from '@/store';
import { isMobile } from './utils'; import { isMobile } from './utils';
const playMusicUrl = computed(() => store.state.playMusicUrl as string);
const play = computed(() => store.state.play as boolean);
onMounted(() => { onMounted(() => {
store.dispatch('initializeSettings'); store.dispatch('initializeSettings');
}); });
-1
View File
@@ -89,7 +89,6 @@ const loadData = async () => {
const { const {
data: { data: dayRecommend }, data: { data: dayRecommend },
} = await getDayRecommend(); } = await getDayRecommend();
console.log('dayRecommend', dayRecommend);
// 处理数据 // 处理数据
if (dayRecommend) { if (dayRecommend) {
singerData.artists = singerData.artists.slice(0, 4); singerData.artists = singerData.artists.slice(0, 4);
+81 -33
View File
@@ -1,5 +1,6 @@
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { audioService } from '@/services/audioService';
import store from '@/store'; import store from '@/store';
import type { ILyricText, SongResult } from '@/type/music'; import type { ILyricText, SongResult } from '@/type/music';
@@ -14,8 +15,34 @@ export const allTime = ref(0); // 总播放时间
export const nowIndex = ref(0); // 当前播放歌词 export const nowIndex = ref(0); // 当前播放歌词
export const correctionTime = ref(0.4); // 歌词矫正时间Correction time export const correctionTime = ref(0.4); // 歌词矫正时间Correction time
export const currentLrcProgress = ref(0); // 来存储当前歌词的进度 export const currentLrcProgress = ref(0); // 来存储当前歌词的进度
export const audio = ref<HTMLAudioElement>(); // 音频对象
export const playMusic = computed(() => store.state.playMusic as SongResult); // 当前播放歌曲 export const playMusic = computed(() => store.state.playMusic as SongResult); // 当前播放歌曲
export const sound = ref<Howl | null>(audioService.getCurrentSound());
document.onkeyup = (e) => {
switch (e.code) {
case 'Space':
if (store.state.play) {
store.commit('setPlayMusic', false);
audioService.getCurrentSound()?.pause();
} else {
store.commit('setPlayMusic', true);
audioService.getCurrentSound()?.play();
}
break;
default:
}
};
watch(
() => store.state.playMusicUrl,
(newVal) => {
if (newVal) {
audioService.play(newVal);
sound.value = audioService.getCurrentSound();
audioServiceOn(audioService);
}
},
);
watch( watch(
() => store.state.playMusic, () => store.state.playMusic,
@@ -29,6 +56,48 @@ watch(
deep: true, deep: true,
}, },
); );
export const audioServiceOn = (audio: typeof audioService) => {
let interval: any = null;
// 监听播放
audio.onPlay(() => {
store.commit('setPlayMusic', true);
interval = setInterval(() => {
nowTime.value = sound.value?.seek() as number;
allTime.value = sound.value?.duration() as number;
const newIndex = getLrcIndex(nowTime.value);
if (newIndex !== nowIndex.value) {
nowIndex.value = newIndex;
currentLrcProgress.value = 0;
}
if (isElectron.value) {
sendLyricToWin();
}
}, 50);
});
// 监听暂停
audio.onPause(() => {
store.commit('setPlayMusic', false);
clearInterval(interval);
});
// 监听结束
audio.onEnd(() => {
handleEnded();
store.commit('nextPlay');
});
};
export const play = () => {
audioService.getCurrentSound()?.play();
};
export const pause = () => {
audioService.getCurrentSound()?.pause();
};
const isPlaying = computed(() => store.state.play as boolean); const isPlaying = computed(() => store.state.play as boolean);
// 增加矫正时间 // 增加矫正时间
@@ -78,25 +147,18 @@ export const getLrcStyle = (index: number) => {
return {}; return {};
}; };
watch(nowTime, (newTime) => {
const newIndex = getLrcIndex(newTime);
if (newIndex !== nowIndex.value) {
nowIndex.value = newIndex;
currentLrcProgress.value = 0; // 重置进度
}
});
// 播放进度 // 播放进度
export const useLyricProgress = () => { export const useLyricProgress = () => {
let animationFrameId: number | null = null; let animationFrameId: number | null = null;
const updateProgress = () => { const updateProgress = () => {
if (!isPlaying.value) return; if (!isPlaying.value) return;
audio.value = audio.value || (document.querySelector('#MusicAudio') as HTMLAudioElement); const currentSound = sound.value;
if (!audio.value) return; if (!currentSound) return;
const { start, end } = currentLrcTiming.value; const { start, end } = currentLrcTiming.value;
const duration = end - start; const duration = end - start;
const elapsed = audio.value.currentTime - start; const elapsed = (currentSound.seek() as number) - start;
currentLrcProgress.value = Math.min(Math.max((elapsed / duration) * 100, 0), 100); currentLrcProgress.value = Math.min(Math.max((elapsed / duration) * 100, 0), 100);
animationFrameId = requestAnimationFrame(updateProgress); animationFrameId = requestAnimationFrame(updateProgress);
@@ -140,9 +202,12 @@ export const useLyricProgress = () => {
}; };
// 设置当前播放时间 // 设置当前播放时间
export const setAudioTime = (index: number, audio: HTMLAudioElement) => { export const setAudioTime = (index: number) => {
audio.currentTime = lrcTimeArray.value[index]; const currentSound = sound.value;
audio.play(); if (!currentSound) return;
currentSound.seek(lrcTimeArray.value[index]);
currentSound.play();
}; };
// 获取当前播放的歌词 // 获取当前播放的歌词
@@ -154,7 +219,7 @@ export const getCurrentLrc = () => {
}; };
}; };
// 获取一句歌词播放时间几秒到几秒 // 获取一句歌词播放时间几秒到几秒
export const getLrcTimeRange = (index: number) => ({ export const getLrcTimeRange = (index: number) => ({
currentTime: lrcTimeArray.value[index], currentTime: lrcTimeArray.value[index],
nextTime: lrcTimeArray.value[index + 1], nextTime: lrcTimeArray.value[index + 1],
@@ -180,26 +245,9 @@ watch(isPlaying, (newIsPlaying) => {
} }
}); });
// 监听时间变化
watch(nowTime, (newTime) => {
const newIndex = getLrcIndex(newTime);
if (newIndex !== nowIndex.value) {
nowIndex.value = newIndex;
currentLrcProgress.value = 0; // 重置进度
// 当索引变化时发送更新
if (isElectron.value) {
sendLyricToWin();
}
}
});
// 处理歌曲结束 // 处理歌曲结束
export const handleEnded = () => { export const handleEnded = () => {
// ... 原有的结束处理逻辑 ...
// 如果有歌词窗口,发送初始化数据
if (isElectron.value) { if (isElectron.value) {
// 延迟一下等待新歌曲加载完成
setTimeout(() => { setTimeout(() => {
initLyricWindow(); initLyricWindow();
sendLyricToWin(); sendLyricToWin();
+20 -3
View File
@@ -1,5 +1,8 @@
import { Howl } from 'howler';
import { getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music'; import { getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music';
import { useMusicHistory } from '@/hooks/MusicHistoryHook'; import { useMusicHistory } from '@/hooks/MusicHistoryHook';
import { audioService } from '@/services/audioService';
import type { ILyric, ILyricText, SongResult } from '@/type/music'; import type { ILyric, ILyricText, SongResult } from '@/type/music';
import { getImgUrl, getMusicProxyUrl } from '@/utils'; import { getImgUrl, getMusicProxyUrl } from '@/utils';
import { getImageLinearBackground } from '@/utils/linearColor'; import { getImageLinearBackground } from '@/utils/linearColor';
@@ -53,9 +56,13 @@ export const useMusicListHook = () => {
// 用于预加载下一首歌曲的 MP3 数据 // 用于预加载下一首歌曲的 MP3 数据
const preloadNextSong = (nextSongUrl: string) => { const preloadNextSong = (nextSongUrl: string) => {
const audio = new Audio(nextSongUrl); const sound = new Howl({
audio.preload = 'auto'; // 设置预加载 src: [nextSongUrl],
audio.load(); // 手动加载 html5: true,
preload: true,
autoplay: false,
});
return sound;
}; };
const fetchSongs = async (state: any, startIndex: number, endIndex: number) => { const fetchSongs = async (state: any, startIndex: number, endIndex: number) => {
@@ -166,9 +173,19 @@ export const useMusicListHook = () => {
state.playMusic.lyric = lyrics; state.playMusic.lyric = lyrics;
}; };
const play = () => {
audioService.getCurrentSound()?.play();
};
const pause = () => {
audioService.getCurrentSound()?.pause();
};
return { return {
handlePlayMusic, handlePlayMusic,
nextPlay, nextPlay,
prevPlay, prevPlay,
play,
pause,
}; };
}; };
-59
View File
@@ -64,68 +64,9 @@ const store = useStore();
const isPlay = computed(() => store.state.isPlay as boolean); const isPlay = computed(() => store.state.isPlay as boolean);
const { menus } = store.state; const { menus } = store.state;
const play = computed(() => store.state.play as boolean);
const route = useRoute(); const route = useRoute();
const audio = {
value: document.querySelector('#MusicAudio') as HTMLAudioElement,
};
const backgroundColor = ref('#000'); const backgroundColor = ref('#000');
// watch(
// () => store.state.playMusic,
// () => {
// backgroundColor.value = store.state.playMusic.backgroundColor;
// console.log('backgroundColor.value', backgroundColor.value);
// },
// {
// immediate: true,
// deep: true,
// },
// );
onMounted(() => {
// 监听音乐是否播放
watch(
() => play.value,
(value) => {
if (value && audio.value) {
audioPlay();
} else {
audioPause();
}
},
);
document.onkeyup = (e) => {
switch (e.code) {
case 'Space':
playMusicEvent();
break;
default:
}
};
});
const audioPlay = () => {
if (audio.value) {
audio.value.play();
}
};
const audioPause = () => {
if (audio.value) {
audio.value.pause();
}
};
const playMusicEvent = async () => {
if (play.value) {
store.commit('setPlayMusic', false);
} else {
store.commit('setPlayMusic', true);
}
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
+5 -5
View File
@@ -34,7 +34,7 @@
:key="index" :key="index"
class="music-lrc-text" class="music-lrc-text"
:class="{ 'now-text': index === nowIndex, 'hover-text': item.text }" :class="{ 'now-text': index === nowIndex, 'hover-text': item.text }"
@click="setAudioTime(index, audio)" @click="setAudioTime(index)"
> >
<span :style="getLrcStyle(index)">{{ item.text }}</span> <span :style="getLrcStyle(index)">{{ item.text }}</span>
<div class="music-lrc-text-tr">{{ item.trText }}</div> <div class="music-lrc-text-tr">{{ item.trText }}</div>
@@ -75,10 +75,6 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
audio: {
type: HTMLAudioElement,
default: null,
},
background: { background: {
type: String, type: String,
default: '', default: '',
@@ -260,6 +256,7 @@ defineExpose({
span { span {
background-clip: text !important; background-clip: text !important;
-webkit-background-clip: text !important; -webkit-background-clip: text !important;
padding-right: 30px;
} }
&-tr { &-tr {
@@ -291,6 +288,9 @@ defineExpose({
.music-lrc { .music-lrc {
height: calc(100vh - 260px) !important; height: calc(100vh - 260px) !important;
width: 100vw; width: 100vw;
span {
padding-right: 0px !important;
}
} }
.music-lrc-text { .music-lrc-text {
text-align: center; text-align: center;
+22 -61
View File
@@ -1,11 +1,6 @@
<template> <template>
<!-- 展开全屏 --> <!-- 展开全屏 -->
<music-full <music-full ref="MusicFullRef" v-model:music-full="musicFullVisible" :background="background" />
ref="MusicFullRef"
v-model:music-full="musicFullVisible"
:audio="audio.value as HTMLAudioElement"
:background="background"
/>
<!-- 底部播放栏 --> <!-- 底部播放栏 -->
<div <div
class="music-play-bar" class="music-play-bar"
@@ -112,11 +107,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useThrottleFn } from '@vueuse/core';
import { useTemplateRef } from 'vue'; import { useTemplateRef } from 'vue';
import { useStore } from 'vuex'; import { useStore } from 'vuex';
import SongItem from '@/components/common/SongItem.vue'; import SongItem from '@/components/common/SongItem.vue';
import { allTime, getCurrentLrc, isElectron, nowTime, openLyric, sendLyricToWin } from '@/hooks/MusicHook'; import { allTime, isElectron, nowTime, openLyric, sound } from '@/hooks/MusicHook';
import type { SongResult } from '@/type/music'; import type { SongResult } from '@/type/music';
import { getImgUrl, secondToMinute, setAnimationClass } from '@/utils'; import { getImgUrl, secondToMinute, setAnimationClass } from '@/utils';
@@ -128,12 +124,8 @@ const store = useStore();
const playMusic = computed(() => store.state.playMusic as SongResult); const playMusic = computed(() => store.state.playMusic as SongResult);
// 是否播放 // 是否播放
const play = computed(() => store.state.play as boolean); const play = computed(() => store.state.play as boolean);
const playList = computed(() => store.state.playList as SongResult[]); const playList = computed(() => store.state.playList as SongResult[]);
const audio = {
value: document.querySelector('#MusicAudio') as HTMLAudioElement,
};
const background = ref('#000'); const background = ref('#000');
watch( watch(
@@ -144,30 +136,28 @@ watch(
{ immediate: true, deep: true }, { immediate: true, deep: true },
); );
const audioPlay = () => { // 使用 useThrottleFn 创建节流版本的 seek 函数
if (audio.value) { const throttledSeek = useThrottleFn((value: number) => {
audio.value.play(); if (!sound.value) return;
} sound.value.seek((value * allTime.value) / 100);
}; store.commit('setPlayMusic', true);
}, 50); // 50ms 的节流延迟
// 计算属性 获取当前播放时间的进度 // 修改 timeSlider 计算属性
const timeSlider = computed({ const timeSlider = computed({
get: () => (nowTime.value / allTime.value) * 100, get: () => (nowTime.value / allTime.value) * 100,
set: (value) => { set: throttledSeek,
if (!audio.value) return;
audio.value.currentTime = (value * allTime.value) / 100;
audioPlay();
store.commit('setPlayMusic', true);
},
}); });
// 音量条 // 音量条
const audioVolume = ref(1); const audioVolume = ref(localStorage.getItem('volume') ? parseFloat(localStorage.getItem('volume') as string) : 1);
const volumeSlider = computed({ const volumeSlider = computed({
get: () => audioVolume.value * 100, get: () => audioVolume.value * 100,
set: (value) => { set: (value) => {
if (!audio.value) return; if (!sound.value) return;
audio.value.volume = value / 100; localStorage.setItem('volume', (value / 100).toString());
sound.value.volume(value / 100);
audioVolume.value = value / 100;
}, },
}); });
// 获取当前播放时间 // 获取当前播放时间
@@ -180,25 +170,6 @@ const getAllTime = computed(() => {
return secondToMinute(allTime.value); return secondToMinute(allTime.value);
}); });
// 监听音乐播放 获取时间
const onAudio = () => {
if (audio.value) {
audio.value.removeEventListener('timeupdate', handleGetAudioTime);
audio.value.removeEventListener('ended', handleEnded);
audio.value.addEventListener('timeupdate', handleGetAudioTime);
audio.value.addEventListener('ended', handleEnded);
// 监听音乐播放暂停
audio.value.addEventListener('pause', () => {
store.commit('setPlayMusic', false);
});
audio.value.addEventListener('play', () => {
store.commit('setPlayMusic', true);
});
}
};
onAudio();
function handleEnded() { function handleEnded() {
store.commit('nextPlay'); store.commit('nextPlay');
} }
@@ -209,27 +180,17 @@ function handlePrev() {
const MusicFullRef = ref<any>(null); const MusicFullRef = ref<any>(null);
function handleGetAudioTime(this: HTMLAudioElement) {
// 监听音频播放的实时时间事件
const audio = this as HTMLAudioElement;
// 获取当前播放时间
nowTime.value = audio.currentTime;
getCurrentLrc();
// 获取总时间
allTime.value = audio.duration;
// 获取音量
audioVolume.value = audio.volume;
sendLyricToWin(store.state.isPlay);
// if (musicFullVisible.value) {
// MusicFullRef.value?.lrcScroll();
// }
}
// 播放暂停按钮事件 // 播放暂停按钮事件
const playMusicEvent = async () => { const playMusicEvent = async () => {
if (play.value) { if (play.value) {
if (sound.value) {
sound.value.pause();
}
store.commit('setPlayMusic', false); store.commit('setPlayMusic', false);
} else { } else {
if (sound.value) {
sound.value.play();
}
store.commit('setPlayMusic', true); store.commit('setPlayMusic', true);
} }
}; };
+55
View File
@@ -0,0 +1,55 @@
import { Howl } from 'howler';
class AudioService {
private currentSound: Howl | null = null;
play(url: string) {
if (this.currentSound) {
this.currentSound.unload();
}
this.currentSound = null;
this.currentSound = new Howl({
src: [url],
html5: true,
autoplay: true,
volume: localStorage.getItem('volume') ? parseFloat(localStorage.getItem('volume') as string) : 1,
});
return this.currentSound;
}
getCurrentSound() {
return this.currentSound;
}
stop() {
if (this.currentSound) {
this.currentSound.stop();
this.currentSound.unload();
this.currentSound = null;
}
}
// 监听播放
onPlay(callback: () => void) {
if (this.currentSound) {
this.currentSound.on('play', callback);
}
}
// 监听暂停
onPause(callback: () => void) {
if (this.currentSound) {
this.currentSound.on('pause', callback);
}
}
// 监听结束
onEnd(callback: () => void) {
if (this.currentSound) {
this.currentSound.on('end', callback);
}
}
}
export const audioService = new AudioService();
+2 -2
View File
@@ -36,10 +36,10 @@ export default defineConfig({
port: 4488, port: 4488,
proxy: { proxy: {
// with options // with options
[process.env.VITE_API_PROXY as string]: { [process.env.VITE_API_LOCAL as string]: {
target: process.env.VITE_API, target: process.env.VITE_API,
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(new RegExp(`^${process.env.VITE_API_PROXY}`), ''), rewrite: (path) => path.replace(new RegExp(`^${process.env.VITE_API_LOCAL}`), ''),
}, },
[process.env.VITE_API_MUSIC_PROXY as string]: { [process.env.VITE_API_MUSIC_PROXY as string]: {
target: process.env.VITE_API_MUSIC, target: process.env.VITE_API_MUSIC,