fix(player): 切歌时封面/歌词/背景色与音频同步就绪

修复 #712 重构引入的体验回退:全屏页点下一首会'先响歌、后换脸'——
歌词/背景色在播放成功后才开始加载。

- playTrack 的元数据加载(歌词+背景色)提前到与取 URL/加载音频并行:
  不阻塞出声,视觉信息通常在起播前就绪;playMusic 被 getSongDetail
  替换后重新应用已到达的元数据,避免丢失;generation 拦截过期请求
- 预加载下一首时顺带预热背景色(歌词已有预热),切歌时元数据全部
  缓存命中、立即应用

测试:时序状态机模拟 3 场景全过(元数据先到不丢失/后到补上/快速
切歌不污染),typecheck 与构建通过。
This commit is contained in:
alger
2026-07-05 16:29:58 +08:00
parent 3742ed5062
commit 968116ce18
2 changed files with 44 additions and 15 deletions
+29 -15
View File
@@ -200,6 +200,32 @@ export const playTrack = async (
// 保存原始歌曲数据
const originalMusic = { ...music };
// 4.5 立即并行加载元数据(歌词 + 背景色),与取 URL/加载音频同时进行:
// 不阻塞出声,但让全屏页的封面/歌词/背景色尽量在音频起播前就绪,
// 避免"先响歌、后换脸"的割裂感。预加载过的歌曲两者都是缓存命中,立即应用
let loadedMetadata: {
lyrics: SongResult['lyric'];
backgroundColor: string;
primaryColor: string;
} | null = null;
const applyLoadedMetadata = () => {
if (!loadedMetadata || gen !== generation) return;
playerCore.playMusic.lyric = loadedMetadata.lyrics;
playerCore.playMusic.backgroundColor = loadedMetadata.backgroundColor;
playerCore.playMusic.primaryColor = loadedMetadata.primaryColor;
// 触发 watcher 更新
playerCore.playMusic = { ...playerCore.playMusic };
};
loadMetadata(originalMusic)
.then((result) => {
loadedMetadata = result;
applyLoadedMetadata();
})
.catch((error) => {
// 元数据加载失败不阻塞播放
console.warn('[playbackController] 元数据加载失败:', error);
});
// 5. 添加到播放历史
try {
const playHistoryStore = await getPlayHistoryStore();
@@ -228,6 +254,8 @@ export const playTrack = async (
playerCore.playMusic = updatedPlayMusic;
playerCore.playMusicUrl = updatedPlayMusic.playMusicUrl as string;
music.playMusicUrl = updatedPlayMusic.playMusicUrl as string;
// playMusic 被替换为新对象,若元数据已先行到达则重新应用,避免歌词/背景色丢失
applyLoadedMetadata();
} catch (error) {
if (gen !== generation) return false;
console.error('[playbackController] 获取歌曲详情失败:', error);
@@ -251,21 +279,7 @@ export const playTrack = async (
}
if (success) {
// 8. 播放成功后,异步加载元数据(不阻塞)
loadMetadata(playerCore.playMusic).then(({ lyrics, backgroundColor, primaryColor }) => {
// 检查 generation(确保还是当前歌曲)
if (gen !== generation) {
console.log(`[playbackController] gen=${gen} 已过期,跳过元数据更新`);
return;
}
playerCore.playMusic.lyric = lyrics;
playerCore.playMusic.backgroundColor = backgroundColor;
playerCore.playMusic.primaryColor = primaryColor;
// 触发 watcher 更新
playerCore.playMusic = { ...playerCore.playMusic };
}).catch((error) => {
console.warn('[playbackController] 元数据加载失败:', error);
});
// 8. 元数据已在 4.5 步与播放并行加载,此处无需再处理
// 9. 播放成功
playerCore.playMusic.playLoading = false;
+15
View File
@@ -84,6 +84,21 @@ export const usePlaylistStore = defineStore(
}
}
// 预热下一首的背景色:切歌时 playTrack 的元数据加载可直接缓存命中,
// 全屏页封面/背景色与音频同步就绪,不再"先响歌、后换背景"
if (nextSong?.picUrl && !(nextSong.backgroundColor && nextSong.primaryColor)) {
try {
const { getImageLinearBackground } = await import('@/utils/linearColor');
const { backgroundColor, primaryColor } = await getImageLinearBackground(
getImgUrl(nextSong.picUrl, '30y30')
);
nextSong.backgroundColor = backgroundColor;
nextSong.primaryColor = primaryColor;
} catch (error) {
console.warn('预热背景色失败:', error);
}
}
detailedSongs.forEach((song, index) => {
if (song && startIndex + index < playList.value.length) {
playList.value[startIndex + index] = song;