mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-14 14:50:50 +08:00
refactor: 统一进度追踪机制,移除重复的rAF更新循环 (H-007/H-008)
- 移除 Mechanism A (rAF + setTimeout 混用),消除定时器泄漏 bug - 将逐字歌词进度计算和 localStorage 保存迁移到 Mechanism B (setInterval 50ms) - 消除 nowTime 竞争写入,从 ~30次/秒 seek 调用降到 20次/秒 - 修复 timer ID 类型 (any -> number)
This commit is contained in:
@@ -27,7 +27,6 @@ export const initMusicHook = (store: ReturnType<typeof usePlayerStore>) => {
|
|||||||
|
|
||||||
// 在 store 注入后初始化需要 store 的功能
|
// 在 store 注入后初始化需要 store 的功能
|
||||||
setupKeyboardListeners();
|
setupKeyboardListeners();
|
||||||
initProgressAnimation();
|
|
||||||
setupMusicWatchers();
|
setupMusicWatchers();
|
||||||
setupCorrectionTimeWatcher();
|
setupCorrectionTimeWatcher();
|
||||||
setupPlayStateWatcher();
|
setupPlayStateWatcher();
|
||||||
@@ -89,181 +88,8 @@ const setupKeyboardListeners = () => {
|
|||||||
|
|
||||||
const { message } = createDiscreteApi(['message']);
|
const { message } = createDiscreteApi(['message']);
|
||||||
|
|
||||||
// 全局变量
|
|
||||||
let progressAnimationInitialized = false;
|
|
||||||
let globalAnimationFrameId: number | null = null;
|
|
||||||
const lastSavedTime = ref(0);
|
|
||||||
let audioListenersInitialized = false;
|
let audioListenersInitialized = false;
|
||||||
|
|
||||||
// 全局停止函数
|
|
||||||
const stopProgressAnimation = () => {
|
|
||||||
if (globalAnimationFrameId) {
|
|
||||||
cancelAnimationFrame(globalAnimationFrameId);
|
|
||||||
globalAnimationFrameId = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 全局更新函数
|
|
||||||
const updateProgress = () => {
|
|
||||||
if (!getPlayerStore().play) {
|
|
||||||
stopProgressAnimation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentSound = sound.value;
|
|
||||||
if (!currentSound) {
|
|
||||||
console.log('进度更新:无效的 sound 对象');
|
|
||||||
// 不是立即返回,而是设置定时器稍后再次尝试
|
|
||||||
globalAnimationFrameId = setTimeout(() => {
|
|
||||||
requestAnimationFrame(updateProgress);
|
|
||||||
}, 100) as unknown as number;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof currentSound.seek !== 'function') {
|
|
||||||
console.log('进度更新:无效的 seek 函数');
|
|
||||||
// 不是立即返回,而是设置定时器稍后再次尝试
|
|
||||||
globalAnimationFrameId = setTimeout(() => {
|
|
||||||
requestAnimationFrame(updateProgress);
|
|
||||||
}, 100) as unknown as number;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { start, end } = currentLrcTiming.value;
|
|
||||||
if (typeof start !== 'number' || typeof end !== 'number' || start === end) {
|
|
||||||
globalAnimationFrameId = requestAnimationFrame(updateProgress);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentTime;
|
|
||||||
try {
|
|
||||||
// 获取当前播放位置
|
|
||||||
currentTime = currentSound.seek() as number;
|
|
||||||
|
|
||||||
// 减少更新频率,避免频繁更新UI
|
|
||||||
const timeDiff = Math.abs(currentTime - nowTime.value);
|
|
||||||
if (timeDiff > 0.2 || Math.floor(currentTime) !== Math.floor(nowTime.value)) {
|
|
||||||
nowTime.value = currentTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存当前播放进度到 localStorage (每秒保存一次,避免频繁写入)
|
|
||||||
if (
|
|
||||||
Math.floor(currentTime) % 2 === 0 &&
|
|
||||||
Math.floor(currentTime) !== Math.floor(lastSavedTime.value)
|
|
||||||
) {
|
|
||||||
lastSavedTime.value = currentTime;
|
|
||||||
if (getPlayerStore().playMusic && getPlayerStore().playMusic.id) {
|
|
||||||
localStorage.setItem(
|
|
||||||
'playProgress',
|
|
||||||
JSON.stringify({
|
|
||||||
songId: getPlayerStore().playMusic.id,
|
|
||||||
progress: currentTime
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (seekError) {
|
|
||||||
console.error('调用 seek() 方法出错:', seekError);
|
|
||||||
globalAnimationFrameId = requestAnimationFrame(updateProgress);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof currentTime !== 'number' || Number.isNaN(currentTime)) {
|
|
||||||
console.error('无效的当前时间:', currentTime);
|
|
||||||
globalAnimationFrameId = requestAnimationFrame(updateProgress);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const elapsed = currentTime - start;
|
|
||||||
const duration = end - start;
|
|
||||||
const progress = (elapsed / duration) * 100;
|
|
||||||
|
|
||||||
// 确保进度在 0-100 之间
|
|
||||||
currentLrcProgress.value = Math.min(Math.max(progress, 0), 100);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新进度出错:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 继续下一帧更新,但降低更新频率为60帧中更新10帧
|
|
||||||
globalAnimationFrameId = setTimeout(() => {
|
|
||||||
requestAnimationFrame(updateProgress);
|
|
||||||
}, 100) as unknown as number;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 全局启动函数
|
|
||||||
const startProgressAnimation = () => {
|
|
||||||
stopProgressAnimation(); // 先停止之前的动画
|
|
||||||
updateProgress();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 全局初始化函数
|
|
||||||
const initProgressAnimation = () => {
|
|
||||||
if (progressAnimationInitialized) return;
|
|
||||||
|
|
||||||
console.log('初始化进度动画');
|
|
||||||
progressAnimationInitialized = true;
|
|
||||||
|
|
||||||
// 监听播放状态变化,这里使用防抖,避免频繁触发
|
|
||||||
let debounceTimer: any = null;
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => getPlayerStore().play,
|
|
||||||
(newIsPlaying) => {
|
|
||||||
console.log('播放状态变化:', newIsPlaying);
|
|
||||||
|
|
||||||
// 清除之前的定时器
|
|
||||||
if (debounceTimer) {
|
|
||||||
clearTimeout(debounceTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用防抖,延迟 100ms 再执行
|
|
||||||
debounceTimer = setTimeout(() => {
|
|
||||||
if (newIsPlaying) {
|
|
||||||
// 确保 sound 对象有效时才启动进度更新
|
|
||||||
if (sound.value) {
|
|
||||||
console.log('sound 对象已存在,立即启动进度更新');
|
|
||||||
startProgressAnimation();
|
|
||||||
} else {
|
|
||||||
console.log('等待 sound 对象初始化...');
|
|
||||||
// 定时检查 sound 对象是否已初始化
|
|
||||||
const checkInterval = setInterval(() => {
|
|
||||||
if (sound.value) {
|
|
||||||
clearInterval(checkInterval);
|
|
||||||
console.log('sound 对象已初始化,开始进度更新');
|
|
||||||
startProgressAnimation();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
// 设置超时,防止无限等待
|
|
||||||
setTimeout(() => {
|
|
||||||
clearInterval(checkInterval);
|
|
||||||
console.log('等待 sound 对象超时,已停止等待');
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
stopProgressAnimation();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听当前歌词索引变化
|
|
||||||
watch(nowIndex, () => {
|
|
||||||
currentLrcProgress.value = 0;
|
|
||||||
if (getPlayerStore().play) {
|
|
||||||
startProgressAnimation();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听音频对象变化
|
|
||||||
watch(sound, (newSound) => {
|
|
||||||
console.log('sound 对象变化:', !!newSound);
|
|
||||||
if (newSound && getPlayerStore().play) {
|
|
||||||
startProgressAnimation();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析歌词字符串并转换为ILyricText格式
|
* 解析歌词字符串并转换为ILyricText格式
|
||||||
* @param lyricsStr 歌词字符串
|
* @param lyricsStr 歌词字符串
|
||||||
@@ -399,10 +225,11 @@ const setupAudioListeners = () => {
|
|||||||
}
|
}
|
||||||
audioListenersInitialized = true;
|
audioListenersInitialized = true;
|
||||||
|
|
||||||
let interval: any = null;
|
let interval: number | null = null;
|
||||||
// 播放状态恢复定时器:当 interval 因异常被清除时,自动恢复
|
// 播放状态恢复定时器:当 interval 因异常被清除时,自动恢复
|
||||||
let recoveryTimer: any = null;
|
let recoveryTimer: number | null = null;
|
||||||
let lyricThrottleCounter = 0;
|
let lyricThrottleCounter = 0;
|
||||||
|
let lastSavedProgress = 0;
|
||||||
|
|
||||||
const clearInterval = () => {
|
const clearInterval = () => {
|
||||||
if (interval) {
|
if (interval) {
|
||||||
@@ -451,14 +278,27 @@ const setupAudioListeners = () => {
|
|||||||
|
|
||||||
nowTime.value = currentTime;
|
nowTime.value = currentTime;
|
||||||
allTime.value = currentSound.duration() as number;
|
allTime.value = currentSound.duration() as number;
|
||||||
|
|
||||||
|
// === 歌词索引更新 ===
|
||||||
const newIndex = getLrcIndex(nowTime.value);
|
const newIndex = getLrcIndex(nowTime.value);
|
||||||
if (newIndex !== nowIndex.value) {
|
if (newIndex !== nowIndex.value) {
|
||||||
nowIndex.value = newIndex;
|
nowIndex.value = newIndex;
|
||||||
|
currentLrcProgress.value = 0; // 换行时重置进度
|
||||||
if (isElectron && isLyricWindowOpen.value) {
|
if (isElectron && isLyricWindowOpen.value) {
|
||||||
sendLyricToWin();
|
sendLyricToWin();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 节流发送轻量歌词进度更新(每 ~200ms / 约每 4 个 tick)
|
|
||||||
|
// === 逐字歌词行内进度 ===
|
||||||
|
const { start, end } = currentLrcTiming.value;
|
||||||
|
if (typeof start === 'number' && typeof end === 'number' && start !== end) {
|
||||||
|
const elapsed = currentTime - start;
|
||||||
|
const duration = end - start;
|
||||||
|
const progress = (elapsed / duration) * 100;
|
||||||
|
currentLrcProgress.value = Math.min(Math.max(progress, 0), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 节流发送轻量歌词进度更新(每 ~200ms / 约每 4 个 tick)===
|
||||||
lyricThrottleCounter++;
|
lyricThrottleCounter++;
|
||||||
if (isElectron && isLyricWindowOpen.value && lyricThrottleCounter % 4 === 0) {
|
if (isElectron && isLyricWindowOpen.value && lyricThrottleCounter % 4 === 0) {
|
||||||
try {
|
try {
|
||||||
@@ -474,6 +314,23 @@ const setupAudioListeners = () => {
|
|||||||
// 忽略发送失败
|
// 忽略发送失败
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === localStorage 进度保存(每 ~2 秒)===
|
||||||
|
if (
|
||||||
|
Math.floor(currentTime) % 2 === 0 &&
|
||||||
|
Math.floor(currentTime) !== Math.floor(lastSavedProgress)
|
||||||
|
) {
|
||||||
|
lastSavedProgress = currentTime;
|
||||||
|
if (getPlayerStore().playMusic?.id) {
|
||||||
|
localStorage.setItem(
|
||||||
|
'playProgress',
|
||||||
|
JSON.stringify({
|
||||||
|
songId: getPlayerStore().playMusic.id,
|
||||||
|
progress: currentTime
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('进度更新 interval 出错:', error);
|
console.error('进度更新 interval 出错:', error);
|
||||||
// 出错时不清除 interval,让下一次 tick 继续尝试
|
// 出错时不清除 interval,让下一次 tick 继续尝试
|
||||||
|
|||||||
Reference in New Issue
Block a user