mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-18 18:20:49 +08:00
✨ feat: 优化播放 修改为howler 修复搜索导致播放无限卡顿问题(#15)
- 优化了整个项目的播放 - 去除audio - 优化歌词页 歌词同步时间 fixes #15
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# 你的接口地址 (必填)
|
||||
VITE_API = ***
|
||||
VITE_API_LOCAL = ***
|
||||
# 音乐破解接口地址
|
||||
VITE_API_MUSIC = ***
|
||||
# 代理地址
|
||||
|
||||
20
README.md
20
README.md
@@ -68,23 +68,21 @@
|
||||
|
||||
```bash
|
||||
# .env.development
|
||||
# 你的接口地址 (必填)
|
||||
VITE_API = ***
|
||||
# 音乐破解接口地址
|
||||
VITE_API_MUSIC = ***
|
||||
# 代理地址
|
||||
VITE_API_PROXY = ***
|
||||
|
||||
|
||||
# 本地运行代理地址
|
||||
VITE_API_PROXY = /api
|
||||
VITE_API_LOCAL = /api
|
||||
VITE_API_MUSIC_PROXY = /music
|
||||
VITE_API_PROXY_MUSIC = /music_proxy
|
||||
|
||||
# 你的接口地址 (必填)
|
||||
VITE_API = ***
|
||||
# 音乐po接口地址
|
||||
VITE_API_MUSIC = ***
|
||||
VITE_API_PROXY = ***
|
||||
|
||||
|
||||
# .env.production
|
||||
# 你的接口地址 (必填)
|
||||
VITE_API = ***
|
||||
# 音乐破解接口地址
|
||||
# 音乐po接口地址
|
||||
VITE_API_MUSIC = ***
|
||||
# 代理地址
|
||||
VITE_API_PROXY = ***
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-store": "^8.1.0"
|
||||
"@types/howler": "^2.2.12",
|
||||
"electron-store": "^8.1.0",
|
||||
"howler": "^2.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss7-compat": "^2.2.4",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div class="app-container" :class="{ mobile: isMobile }">
|
||||
<audio id="MusicAudio" ref="audioRef" :src="playMusicUrl" :autoplay="play"></audio>
|
||||
<n-config-provider :theme="darkTheme">
|
||||
<n-dialog-provider>
|
||||
<router-view></router-view>
|
||||
@@ -11,15 +10,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { darkTheme } from 'naive-ui';
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
import store from '@/store';
|
||||
|
||||
import { isMobile } from './utils';
|
||||
|
||||
const playMusicUrl = computed(() => store.state.playMusicUrl as string);
|
||||
const play = computed(() => store.state.play as boolean);
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('initializeSettings');
|
||||
});
|
||||
|
||||
@@ -89,7 +89,6 @@ const loadData = async () => {
|
||||
const {
|
||||
data: { data: dayRecommend },
|
||||
} = await getDayRecommend();
|
||||
console.log('dayRecommend', dayRecommend);
|
||||
// 处理数据
|
||||
if (dayRecommend) {
|
||||
singerData.artists = singerData.artists.slice(0, 4);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { audioService } from '@/services/audioService';
|
||||
import store from '@/store';
|
||||
import type { ILyricText, SongResult } from '@/type/music';
|
||||
|
||||
@@ -14,8 +15,34 @@ export const allTime = ref(0); // 总播放时间
|
||||
export const nowIndex = ref(0); // 当前播放歌词
|
||||
export const correctionTime = ref(0.4); // 歌词矫正时间Correction time
|
||||
export const currentLrcProgress = ref(0); // 来存储当前歌词的进度
|
||||
export const audio = ref<HTMLAudioElement>(); // 音频对象
|
||||
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(
|
||||
() => store.state.playMusic,
|
||||
@@ -29,6 +56,48 @@ watch(
|
||||
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);
|
||||
|
||||
// 增加矫正时间
|
||||
@@ -78,25 +147,18 @@ export const getLrcStyle = (index: number) => {
|
||||
return {};
|
||||
};
|
||||
|
||||
watch(nowTime, (newTime) => {
|
||||
const newIndex = getLrcIndex(newTime);
|
||||
if (newIndex !== nowIndex.value) {
|
||||
nowIndex.value = newIndex;
|
||||
currentLrcProgress.value = 0; // 重置进度
|
||||
}
|
||||
});
|
||||
|
||||
// 播放进度
|
||||
export const useLyricProgress = () => {
|
||||
let animationFrameId: number | null = null;
|
||||
|
||||
const updateProgress = () => {
|
||||
if (!isPlaying.value) return;
|
||||
audio.value = audio.value || (document.querySelector('#MusicAudio') as HTMLAudioElement);
|
||||
if (!audio.value) return;
|
||||
const currentSound = sound.value;
|
||||
if (!currentSound) return;
|
||||
|
||||
const { start, end } = currentLrcTiming.value;
|
||||
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);
|
||||
|
||||
animationFrameId = requestAnimationFrame(updateProgress);
|
||||
@@ -140,9 +202,12 @@ export const useLyricProgress = () => {
|
||||
};
|
||||
|
||||
// 设置当前播放时间
|
||||
export const setAudioTime = (index: number, audio: HTMLAudioElement) => {
|
||||
audio.currentTime = lrcTimeArray.value[index];
|
||||
audio.play();
|
||||
export const setAudioTime = (index: number) => {
|
||||
const currentSound = sound.value;
|
||||
if (!currentSound) return;
|
||||
|
||||
currentSound.seek(lrcTimeArray.value[index]);
|
||||
currentSound.play();
|
||||
};
|
||||
|
||||
// 获取当前播放的歌词
|
||||
@@ -154,7 +219,7 @@ export const getCurrentLrc = () => {
|
||||
};
|
||||
};
|
||||
|
||||
// 获取一句歌词播放时间是 几秒到几秒
|
||||
// 获取一句歌词播放时间几秒到几秒
|
||||
export const getLrcTimeRange = (index: number) => ({
|
||||
currentTime: lrcTimeArray.value[index],
|
||||
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 = () => {
|
||||
// ... 原有的结束处理逻辑 ...
|
||||
|
||||
// 如果有歌词窗口,发送初始化数据
|
||||
if (isElectron.value) {
|
||||
// 延迟一下等待新歌曲加载完成
|
||||
setTimeout(() => {
|
||||
initLyricWindow();
|
||||
sendLyricToWin();
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Howl } from 'howler';
|
||||
|
||||
import { getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music';
|
||||
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
|
||||
import { audioService } from '@/services/audioService';
|
||||
import type { ILyric, ILyricText, SongResult } from '@/type/music';
|
||||
import { getImgUrl, getMusicProxyUrl } from '@/utils';
|
||||
import { getImageLinearBackground } from '@/utils/linearColor';
|
||||
@@ -53,9 +56,13 @@ export const useMusicListHook = () => {
|
||||
|
||||
// 用于预加载下一首歌曲的 MP3 数据
|
||||
const preloadNextSong = (nextSongUrl: string) => {
|
||||
const audio = new Audio(nextSongUrl);
|
||||
audio.preload = 'auto'; // 设置预加载
|
||||
audio.load(); // 手动加载
|
||||
const sound = new Howl({
|
||||
src: [nextSongUrl],
|
||||
html5: true,
|
||||
preload: true,
|
||||
autoplay: false,
|
||||
});
|
||||
return sound;
|
||||
};
|
||||
|
||||
const fetchSongs = async (state: any, startIndex: number, endIndex: number) => {
|
||||
@@ -166,9 +173,19 @@ export const useMusicListHook = () => {
|
||||
state.playMusic.lyric = lyrics;
|
||||
};
|
||||
|
||||
const play = () => {
|
||||
audioService.getCurrentSound()?.play();
|
||||
};
|
||||
|
||||
const pause = () => {
|
||||
audioService.getCurrentSound()?.pause();
|
||||
};
|
||||
|
||||
return {
|
||||
handlePlayMusic,
|
||||
nextPlay,
|
||||
prevPlay,
|
||||
play,
|
||||
pause,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -64,68 +64,9 @@ const store = useStore();
|
||||
|
||||
const isPlay = computed(() => store.state.isPlay as boolean);
|
||||
const { menus } = store.state;
|
||||
const play = computed(() => store.state.play as boolean);
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const audio = {
|
||||
value: document.querySelector('#MusicAudio') as HTMLAudioElement,
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
:key="index"
|
||||
class="music-lrc-text"
|
||||
:class="{ 'now-text': index === nowIndex, 'hover-text': item.text }"
|
||||
@click="setAudioTime(index, audio)"
|
||||
@click="setAudioTime(index)"
|
||||
>
|
||||
<span :style="getLrcStyle(index)">{{ item.text }}</span>
|
||||
<div class="music-lrc-text-tr">{{ item.trText }}</div>
|
||||
@@ -75,10 +75,6 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
audio: {
|
||||
type: HTMLAudioElement,
|
||||
default: null,
|
||||
},
|
||||
background: {
|
||||
type: String,
|
||||
default: '',
|
||||
@@ -260,6 +256,7 @@ defineExpose({
|
||||
span {
|
||||
background-clip: text !important;
|
||||
-webkit-background-clip: text !important;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
&-tr {
|
||||
@@ -291,6 +288,9 @@ defineExpose({
|
||||
.music-lrc {
|
||||
height: calc(100vh - 260px) !important;
|
||||
width: 100vw;
|
||||
span {
|
||||
padding-right: 0px !important;
|
||||
}
|
||||
}
|
||||
.music-lrc-text {
|
||||
text-align: center;
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<template>
|
||||
<!-- 展开全屏 -->
|
||||
<music-full
|
||||
ref="MusicFullRef"
|
||||
v-model:music-full="musicFullVisible"
|
||||
:audio="audio.value as HTMLAudioElement"
|
||||
:background="background"
|
||||
/>
|
||||
<music-full ref="MusicFullRef" v-model:music-full="musicFullVisible" :background="background" />
|
||||
<!-- 底部播放栏 -->
|
||||
<div
|
||||
class="music-play-bar"
|
||||
@@ -112,11 +107,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useThrottleFn } from '@vueuse/core';
|
||||
import { useTemplateRef } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
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 { getImgUrl, secondToMinute, setAnimationClass } from '@/utils';
|
||||
|
||||
@@ -128,12 +124,8 @@ const store = useStore();
|
||||
const playMusic = computed(() => store.state.playMusic as SongResult);
|
||||
// 是否播放
|
||||
const play = computed(() => store.state.play as boolean);
|
||||
|
||||
const playList = computed(() => store.state.playList as SongResult[]);
|
||||
|
||||
const audio = {
|
||||
value: document.querySelector('#MusicAudio') as HTMLAudioElement,
|
||||
};
|
||||
const background = ref('#000');
|
||||
|
||||
watch(
|
||||
@@ -144,30 +136,28 @@ watch(
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
|
||||
const audioPlay = () => {
|
||||
if (audio.value) {
|
||||
audio.value.play();
|
||||
}
|
||||
};
|
||||
// 使用 useThrottleFn 创建节流版本的 seek 函数
|
||||
const throttledSeek = useThrottleFn((value: number) => {
|
||||
if (!sound.value) return;
|
||||
sound.value.seek((value * allTime.value) / 100);
|
||||
store.commit('setPlayMusic', true);
|
||||
}, 50); // 50ms 的节流延迟
|
||||
|
||||
// 计算属性 获取当前播放时间的进度
|
||||
// 修改 timeSlider 计算属性
|
||||
const timeSlider = computed({
|
||||
get: () => (nowTime.value / allTime.value) * 100,
|
||||
set: (value) => {
|
||||
if (!audio.value) return;
|
||||
audio.value.currentTime = (value * allTime.value) / 100;
|
||||
audioPlay();
|
||||
store.commit('setPlayMusic', true);
|
||||
},
|
||||
set: throttledSeek,
|
||||
});
|
||||
|
||||
// 音量条
|
||||
const audioVolume = ref(1);
|
||||
const audioVolume = ref(localStorage.getItem('volume') ? parseFloat(localStorage.getItem('volume') as string) : 1);
|
||||
const volumeSlider = computed({
|
||||
get: () => audioVolume.value * 100,
|
||||
set: (value) => {
|
||||
if (!audio.value) return;
|
||||
audio.value.volume = value / 100;
|
||||
if (!sound.value) return;
|
||||
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);
|
||||
});
|
||||
|
||||
// 监听音乐播放 获取时间
|
||||
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() {
|
||||
store.commit('nextPlay');
|
||||
}
|
||||
@@ -209,27 +180,17 @@ function handlePrev() {
|
||||
|
||||
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 () => {
|
||||
if (play.value) {
|
||||
if (sound.value) {
|
||||
sound.value.pause();
|
||||
}
|
||||
store.commit('setPlayMusic', false);
|
||||
} else {
|
||||
if (sound.value) {
|
||||
sound.value.play();
|
||||
}
|
||||
store.commit('setPlayMusic', true);
|
||||
}
|
||||
};
|
||||
|
||||
55
src/services/audioService.ts
Normal file
55
src/services/audioService.ts
Normal 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();
|
||||
@@ -36,10 +36,10 @@ export default defineConfig({
|
||||
port: 4488,
|
||||
proxy: {
|
||||
// with options
|
||||
[process.env.VITE_API_PROXY as string]: {
|
||||
[process.env.VITE_API_LOCAL as string]: {
|
||||
target: process.env.VITE_API,
|
||||
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]: {
|
||||
target: process.env.VITE_API_MUSIC,
|
||||
|
||||
Reference in New Issue
Block a user