🐞 fix: 优化播放处理

This commit is contained in:
imsyy
2025-11-21 11:03:36 +08:00
parent 8866996e5b
commit d527b076dc
4 changed files with 116 additions and 177 deletions

View File

@@ -30,6 +30,10 @@
- 欢迎各位大佬 `Star` 😍 - 欢迎各位大佬 `Star` 😍
## 💬 交流群
![交流群](/screenshots/welcome.png)
## 👀 Demo ## 👀 Demo
- [SPlayer](https://music.imsyy.top/) - [SPlayer](https://music.imsyy.top/)

BIN
screenshots/welcome.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -105,59 +105,51 @@ class LyricManager {
const isStale = () => this.activeLyricReq !== req || musicStore.playSong?.id !== id; const isStale = () => this.activeLyricReq !== req || musicStore.playSong?.id !== id;
// 处理 TTML 歌词 // 处理 TTML 歌词
const adoptTTML = async () => { const adoptTTML = async () => {
try { if (!settingStore.enableTTMLLyric) return;
if (!settingStore.enableTTMLLyric) return; const ttmlContent = await songLyricTTML(id);
const ttmlContent = await songLyricTTML(id); if (isStale()) return;
if (isStale()) return; if (!ttmlContent || typeof ttmlContent !== "string") return;
if (!ttmlContent || typeof ttmlContent !== "string") return; const parsed = parseTTML(ttmlContent);
const parsed = parseTTML(ttmlContent); const lines = parsed?.lines || [];
const lines = parsed?.lines || []; if (!lines.length) return;
if (!lines.length) return; result.yrcData = lines;
result.yrcData = lines; ttmlAdopted = true;
ttmlAdopted = true;
} catch (err) {
throw err;
}
}; };
// 处理 LRC 歌词 // 处理 LRC 歌词
const adoptLRC = async () => { const adoptLRC = async () => {
try { const data = await songLyric(id);
const data = await songLyric(id); if (isStale()) return;
if (isStale()) return; if (!data || data.code !== 200) return;
if (!data || data.code !== 200) return; let lrcLines: LyricLine[] = [];
let lrcLines: LyricLine[] = []; let yrcLines: LyricLine[] = [];
let yrcLines: LyricLine[] = []; // 普通歌词
// 普通歌词 if (data?.lrc?.lyric) {
if (data?.lrc?.lyric) { lrcLines = parseLrc(data.lrc.lyric) || [];
lrcLines = parseLrc(data.lrc.lyric) || []; // 普通歌词翻译
// 普通歌词翻译 if (data?.tlyric?.lyric)
if (data?.tlyric?.lyric) lrcLines = this.alignLyrics(lrcLines, parseLrc(data.tlyric.lyric), "translatedLyric");
lrcLines = this.alignLyrics(lrcLines, parseLrc(data.tlyric.lyric), "translatedLyric"); // 普通歌词音译
// 普通歌词音译 if (data?.romalrc?.lyric)
if (data?.romalrc?.lyric) lrcLines = this.alignLyrics(lrcLines, parseLrc(data.romalrc.lyric), "romanLyric");
lrcLines = this.alignLyrics(lrcLines, parseLrc(data.romalrc.lyric), "romanLyric");
}
// 逐字歌词
if (data?.yrc?.lyric) {
yrcLines = parseYrc(data.yrc.lyric) || [];
// 逐字歌词翻译
if (data?.ytlrc?.lyric)
yrcLines = this.alignLyrics(yrcLines, parseLrc(data.ytlrc.lyric), "translatedLyric");
// 逐字歌词音译
if (data?.yromalrc?.lyric)
yrcLines = this.alignLyrics(yrcLines, parseLrc(data.yromalrc.lyric), "romanLyric");
}
if (lrcLines.length) result.lrcData = lrcLines;
// 如果没有 TTML则采用 网易云 YRC
if (!result.yrcData.length && yrcLines.length) {
result.yrcData = yrcLines;
}
// 先返回一次,避免 TTML 请求过慢
const lyricData = this.handleLyricExclude(result);
this.setFinalLyric(lyricData, req);
} catch (err) {
throw err;
} }
// 逐字歌词
if (data?.yrc?.lyric) {
yrcLines = parseYrc(data.yrc.lyric) || [];
// 逐字歌词翻译
if (data?.ytlrc?.lyric)
yrcLines = this.alignLyrics(yrcLines, parseLrc(data.ytlrc.lyric), "translatedLyric");
// 逐字歌词音译
if (data?.yromalrc?.lyric)
yrcLines = this.alignLyrics(yrcLines, parseLrc(data.yromalrc.lyric), "romanLyric");
}
if (lrcLines.length) result.lrcData = lrcLines;
// 如果没有 TTML则采用 网易云 YRC
if (!result.yrcData.length && yrcLines.length) {
result.yrcData = yrcLines;
}
// 先返回一次,避免 TTML 请求过慢
const lyricData = this.handleLyricExclude(result);
this.setFinalLyric(lyricData, req);
}; };
// 设置 TTML // 设置 TTML
await Promise.allSettled([adoptTTML(), adoptLRC()]); await Promise.allSettled([adoptTTML(), adoptLRC()]);

View File

@@ -3,6 +3,7 @@ import type { MessageReactive } from "naive-ui";
import { Howl, Howler } from "howler"; import { Howl, Howler } from "howler";
import { cloneDeep } from "lodash-es"; import { cloneDeep } from "lodash-es";
import { useMusicStore, useStatusStore, useDataStore, useSettingStore } from "@/stores"; import { useMusicStore, useStatusStore, useDataStore, useSettingStore } from "@/stores";
import { useIntervalFn } from "@vueuse/core";
import { calculateProgress } from "./time"; import { calculateProgress } from "./time";
import { shuffleArray, runIdle } from "./helper"; import { shuffleArray, runIdle } from "./helper";
import { heartRateList } from "@/api/playlist"; import { heartRateList } from "@/api/playlist";
@@ -23,8 +24,11 @@ import audioContextManager from "@/utils/player-utils/context";
import lyricManager from "./lyricManager"; import lyricManager from "./lyricManager";
import blob from "./blob"; import blob from "./blob";
// 播放器核心 /**
// Howler.js * 播放器核心
* Howler.js 音频库
*/
let _player: Player | null = null;
/* *允许播放格式 */ /* *允许播放格式 */
const allowPlayFormat = ["mp3", "flac", "webm", "ogg", "wav"]; const allowPlayFormat = ["mp3", "flac", "webm", "ogg", "wav"];
@@ -33,78 +37,12 @@ class Player {
/** 播放器 */ /** 播放器 */
private player: Howl; private player: Howl;
/** 定时器 */ /** 定时器 */
private playerInterval: ReturnType<typeof setInterval> | undefined; private readonly playerInterval = useIntervalFn(
/** 自动关闭定时器 */ () => {
private autoCloseInterval: ReturnType<typeof setInterval> | undefined; if (!this.player?.playing()) return;
/** 频谱数据 */ const musicStore = useMusicStore();
private audioContext: AudioContext | null = null; const statusStore = useStatusStore();
private analyser: AnalyserNode | null = null; const settingStore = useSettingStore();
private dataArray: Uint8Array<ArrayBuffer> | null = null;
/** 其他数据 */
private message: MessageReactive | null = null;
/** 预载下一首歌曲播放地址缓存(仅存 URL不创建 Howl */
private nextPrefetch: { id: number; url: string | null; ublock: boolean } | null = null;
/** 并发控制:当前播放会话与初始化/切曲状态 */
private playSessionId: number = 0;
/** 是否正在切换歌曲 */
private switching: boolean = false;
/** 当前曲目重试信息(按歌曲维度计数) */
private retryInfo: { songId: number; count: number } = { songId: 0, count: 0 };
constructor() {
// 创建播放器实例
this.player = new Howl({ src: [""], format: allowPlayFormat, autoplay: false });
// 初始化媒体会话
this.initMediaSession();
// 挂载全局
window.$player = this;
}
/**
* 新建会话并返回会话 id
*/
private newSession(): number {
this.playSessionId += 1;
return this.playSessionId;
}
/**
* 检查传入会话是否过期
*/
private isStale(sessionId: number): boolean {
return sessionId !== this.playSessionId;
}
/**
* 重置底层播放器与定时器(幂等)
*/
private resetPlayerCore() {
try {
// 仅卸载当前播放器实例
if (this.player) {
this.player.stop();
this.player.off();
this.player.unload();
}
} catch {
/* empty */
}
this.cleanupAllTimers();
}
/**
* 处理播放状态
*/
private handlePlayStatus() {
const musicStore = useMusicStore();
const statusStore = useStatusStore();
const settingStore = useSettingStore();
const currentSessionId = this.playSessionId;
// 清理定时器
clearInterval(this.playerInterval);
// 更新播放状态
this.playerInterval = setInterval(() => {
// 检查会话是否过期
if (currentSessionId !== this.playSessionId) {
clearInterval(this.playerInterval);
return;
}
if (!this.player.playing()) return;
const currentTime = this.getSeek(); const currentTime = this.getSeek();
const duration = this.getDuration(); const duration = this.getDuration();
// 计算进度条距离 // 计算进度条距离
@@ -130,7 +68,47 @@ class Player {
window.electron.ipcRenderer.send("set-bar", progress); window.electron.ipcRenderer.send("set-bar", progress);
} }
} }
}, 250); },
250,
{ immediate: false },
);
/** 自动关闭定时器 */
private autoCloseInterval: ReturnType<typeof setInterval> | undefined;
/** 频谱数据 */
private audioContext: AudioContext | null = null;
private analyser: AnalyserNode | null = null;
private dataArray: Uint8Array<ArrayBuffer> | null = null;
/** 其他数据 */
private message: MessageReactive | null = null;
/** 预载下一首歌曲播放地址缓存(仅存 URL不创建 Howl */
private nextPrefetch: { id: number; url: string | null; ublock: boolean } | null = null;
/** 当前曲目重试信息(按歌曲维度计数) */
private retryInfo: { songId: number; count: number } = { songId: 0, count: 0 };
constructor() {
// 创建播放器实例
this.player = new Howl({ src: [""], format: allowPlayFormat, autoplay: false });
// 初始化媒体会话
this.initMediaSession();
// 挂载全局
window.$player = this;
}
/**
* 重置底层播放器与定时器(幂等)
*/
private resetPlayerCore() {
try {
// 仅卸载当前播放器实例
if (this.player) {
this.player.stop();
this.player.off();
this.player.unload();
}
Howler.unload();
} catch {
/* empty */
}
this.cleanupAllTimers();
} }
/** /**
* 预载下一首歌曲的播放地址(优先官方,失败则并发尝试解灰) * 预载下一首歌曲的播放地址(优先官方,失败则并发尝试解灰)
@@ -215,19 +193,10 @@ class Player {
const settingStore = useSettingStore(); const settingStore = useSettingStore();
// 播放信息 // 播放信息
const { id, path, type } = musicStore.playSong; const { id, path, type } = musicStore.playSong;
const currentSessionId = this.playSessionId;
// 检查会话是否过期
if (currentSessionId !== this.playSessionId) {
console.log("🚫 Session expired, skipping player creation");
return;
}
// 统一重置底层播放器 // 统一重置底层播放器
this.resetPlayerCore(); this.resetPlayerCore();
// 二次检查会话
if (currentSessionId !== this.playSessionId) {
console.log("🚫 Session expired after cleanup, aborting");
return;
}
// 创建播放器 // 创建播放器
this.player = new Howl({ this.player = new Howl({
src, src,
@@ -250,8 +219,7 @@ class Player {
// else resetSongLyric(); // else resetSongLyric();
// 获取歌词数据 // 获取歌词数据
lyricManager.handleLyric(id, path); lyricManager.handleLyric(id, path);
// 定时获取状态
if (!this.playerInterval) this.handlePlayStatus();
// 新增播放历史 // 新增播放历史
if (type !== "radio") dataStore.setHistory(musicStore.playSong); if (type !== "radio") dataStore.setHistory(musicStore.playSong);
// 获取歌曲封面主色 // 获取歌曲封面主色
@@ -279,10 +247,8 @@ class Player {
const playSongData = getPlaySongData(); const playSongData = getPlaySongData();
// 获取配置 // 获取配置
const { seek } = options; const { seek } = options;
const currentSessionId = this.playSessionId;
// 初次加载 // 初次加载
this.player.once("load", () => { this.player.once("load", () => {
if (currentSessionId !== this.playSessionId) return;
// 允许跨域 // 允许跨域
if (settingStore.showSpectrums) { if (settingStore.showSpectrums) {
const audioDom = this.getAudioDom(); const audioDom = this.getAudioDom();
@@ -325,8 +291,8 @@ class Player {
}); });
// 播放 // 播放
this.player.on("play", () => { this.player.on("play", () => {
if (currentSessionId !== this.playSessionId) return;
window.document.title = getPlayerInfo() || "SPlayer"; window.document.title = getPlayerInfo() || "SPlayer";
this.playerInterval.resume();
// 重置重试计数 // 重置重试计数
try { try {
const current = getPlaySongData(); const current = getPlaySongData();
@@ -344,8 +310,8 @@ class Player {
}); });
// 暂停 // 暂停
this.player.on("pause", () => { this.player.on("pause", () => {
if (currentSessionId !== this.playSessionId) return;
if (!isElectron) window.document.title = "SPlayer"; if (!isElectron) window.document.title = "SPlayer";
this.playerInterval.pause();
// ipc // ipc
if (isElectron) { if (isElectron) {
window.electron.ipcRenderer.send("play-status-change", false); window.electron.ipcRenderer.send("play-status-change", false);
@@ -354,7 +320,7 @@ class Player {
}); });
// 结束 // 结束
this.player.on("end", () => { this.player.on("end", () => {
if (currentSessionId !== this.playSessionId) return; this.playerInterval.pause();
// statusStore.playStatus = false; // statusStore.playStatus = false;
console.log("⏹️ song end:", playSongData); console.log("⏹️ song end:", playSongData);
@@ -374,13 +340,11 @@ class Player {
}); });
// 错误 // 错误
this.player.on("loaderror", (sourceid, err: unknown) => { this.player.on("loaderror", (sourceid, err: unknown) => {
if (currentSessionId !== this.playSessionId) return;
const code = typeof err === "number" ? err : undefined; const code = typeof err === "number" ? err : undefined;
this.handlePlaybackError(code); this.handlePlaybackError(code);
console.error("❌ song error:", sourceid, playSongData, err); console.error("❌ song error:", sourceid, playSongData, err);
}); });
this.player.on("playerror", (sourceid, err: unknown) => { this.player.on("playerror", (sourceid, err: unknown) => {
if (currentSessionId !== this.playSessionId) return;
const code = typeof err === "number" ? err : undefined; const code = typeof err === "number" ? err : undefined;
this.handlePlaybackError(code); this.handlePlaybackError(code);
console.error("❌ song play error:", sourceid, playSongData, err); console.error("❌ song play error:", sourceid, playSongData, err);
@@ -507,7 +471,6 @@ class Player {
} }
// 超过次数:切到下一首或清空 // 超过次数:切到下一首或清空
this.retryInfo.count = 0; this.retryInfo.count = 0;
this.switching = false;
if (dataStore.playList.length > 1) { if (dataStore.playList.length > 1) {
window.$message.error("当前歌曲播放失败,已跳至下一首"); window.$message.error("当前歌曲播放失败,已跳至下一首");
await this.nextOrPrev("next"); await this.nextOrPrev("next");
@@ -589,7 +552,6 @@ class Player {
const musicStore = useMusicStore(); const musicStore = useMusicStore();
const statusStore = useStatusStore(); const statusStore = useStatusStore();
const settingStore = useSettingStore(); const settingStore = useSettingStore();
const sessionId = this.newSession();
try { try {
// 获取播放数据 // 获取播放数据
@@ -609,7 +571,6 @@ class Player {
// 本地歌曲 // 本地歌曲
if (path) { if (path) {
if (this.isStale(sessionId)) return;
try { try {
await this.createPlayer(`file://${path}`, autoPlay, seek); await this.createPlayer(`file://${path}`, autoPlay, seek);
await this.parseLocalMusicInfo(path); await this.parseLocalMusicInfo(path);
@@ -658,20 +619,18 @@ class Player {
if (!playerUrl) { if (!playerUrl) {
window.$message.error("该歌曲暂无音源,跳至下一首"); window.$message.error("该歌曲暂无音源,跳至下一首");
this.switching = false;
await this.nextOrPrev("next"); await this.nextOrPrev("next");
return; return;
} }
} catch (err) { } catch (err) {
console.error("❌ 获取歌曲地址出错:", err); console.error("❌ 获取歌曲地址出错:", err);
window.$message.error("获取歌曲地址失败,跳至下一首"); window.$message.error("获取歌曲地址失败,跳至下一首");
this.switching = false;
await this.nextOrPrev("next"); await this.nextOrPrev("next");
return; return;
} }
// 有有效 URL 才创建播放器 // 有有效 URL 才创建播放器
if (playerUrl && !this.isStale(sessionId)) { if (playerUrl) {
try { try {
await this.createPlayer(playerUrl, autoPlay, seek); await this.createPlayer(playerUrl, autoPlay, seek);
} catch (err) { } catch (err) {
@@ -682,10 +641,7 @@ class Player {
} catch (err) { } catch (err) {
console.error("❌ 初始化音乐播放器出错:", err); console.error("❌ 初始化音乐播放器出错:", err);
window.$message.error("播放遇到错误,尝试下一首"); window.$message.error("播放遇到错误,尝试下一首");
this.switching = false;
await this.nextOrPrev("next"); await this.nextOrPrev("next");
} finally {
this.switching = false;
} }
} }
/** /**
@@ -758,12 +714,6 @@ class Player {
const dataStore = useDataStore(); const dataStore = useDataStore();
const musicStore = useMusicStore(); const musicStore = useMusicStore();
try { try {
if (this.switching) {
console.log("🔄 Already switching, ignoring request");
return;
}
this.switching = true;
// 立即更新UI状态防止用户重复点击 // 立即更新UI状态防止用户重复点击
statusStore.playLoading = true; statusStore.playLoading = true;
statusStore.playStatus = false; statusStore.playStatus = false;
@@ -818,16 +768,12 @@ class Player {
// 重置播放进度(切换歌曲时必须重置) // 重置播放进度(切换歌曲时必须重置)
statusStore.currentTime = 0; statusStore.currentTime = 0;
statusStore.progress = 0; statusStore.progress = 0;
// 暂停当前播放
await this.pause(false);
// 初始化播放器不传入seek参数确保从头开始播放 // 初始化播放器不传入seek参数确保从头开始播放
await this.initPlayer(play, 0); await this.initPlayer(play, 0);
} catch (error) { } catch (error) {
console.error("Error in nextOrPrev:", error); console.error("Error in nextOrPrev:", error);
statusStore.playLoading = false; statusStore.playLoading = false;
throw error; throw error;
} finally {
this.switching = false;
} }
} }
/** /**
@@ -1046,7 +992,6 @@ class Player {
// 查找索引(在处理后的列表中查找) // 查找索引(在处理后的列表中查找)
statusStore.playIndex = processedData.findIndex((item) => item.id === song.id); statusStore.playIndex = processedData.findIndex((item) => item.id === song.id);
// 播放 // 播放
await this.pause(false);
await this.initPlayer(); await this.initPlayer();
} }
} else { } else {
@@ -1055,7 +1000,6 @@ class Player {
? Math.floor(Math.random() * processedData.length) ? Math.floor(Math.random() * processedData.length)
: 0; : 0;
// 播放 // 播放
await this.pause(false);
await this.initPlayer(); await this.initPlayer();
} }
// 更改播放歌单 // 更改播放歌单
@@ -1095,11 +1039,6 @@ class Player {
const dataStore = useDataStore(); const dataStore = useDataStore();
const statusStore = useStatusStore(); const statusStore = useStatusStore();
try { try {
if (this.switching) {
console.log("🔄 Already switching, ignoring request");
return;
}
this.switching = true;
// 立即更新UI状态防止用户重复点击 // 立即更新UI状态防止用户重复点击
statusStore.playLoading = true; statusStore.playLoading = true;
statusStore.playStatus = false; statusStore.playStatus = false;
@@ -1118,8 +1057,6 @@ class Player {
statusStore.currentTime = 0; statusStore.currentTime = 0;
statusStore.progress = 0; statusStore.progress = 0;
statusStore.lyricIndex = -1; statusStore.lyricIndex = -1;
// 暂停当前播放
await this.pause(false);
// 清理定时器,防止旧定时器继续运行 // 清理定时器,防止旧定时器继续运行
this.cleanupAllTimers(); this.cleanupAllTimers();
// 清理并播放不传入seek参数确保从头开始播放 // 清理并播放不传入seek参数确保从头开始播放
@@ -1128,8 +1065,6 @@ class Player {
console.error("Error in togglePlayIndex:", error); console.error("Error in togglePlayIndex:", error);
statusStore.playLoading = false; statusStore.playLoading = false;
throw error; throw error;
} finally {
this.switching = false;
} }
} }
/** /**
@@ -1469,9 +1404,8 @@ class Player {
*/ */
private cleanupAllTimers() { private cleanupAllTimers() {
// 清理播放状态定时器 // 清理播放状态定时器
if (this.playerInterval) { if (this.playerInterval.isActive.value) {
clearInterval(this.playerInterval); this.playerInterval.pause();
this.playerInterval = undefined;
} }
// 清理自动关闭定时器 // 清理自动关闭定时器
if (this.autoCloseInterval) { if (this.autoCloseInterval) {
@@ -1495,3 +1429,12 @@ class Player {
} }
export default new Player(); export default new Player();
/**
* 获取播放器实例
* @returns Player
*/
export const usePlayer = (): Player => {
if (!_player) _player = new Player();
return _player;
};