🔧 chore:改进播放器组件的加载状态显示, 优化 GD音乐解析逻辑,增加超时处理,调整音源列表

This commit is contained in:
alger
2025-05-10 20:12:10 +08:00
parent 80450349c0
commit 9cc064c01b
5 changed files with 271 additions and 210 deletions
+23 -5
View File
@@ -28,14 +28,26 @@ export interface ParsedMusicResult {
* @param id 音乐ID * @param id 音乐ID
* @param data 音乐数据,包含名称和艺术家信息 * @param data 音乐数据,包含名称和艺术家信息
* @param quality 音质设置 * @param quality 音质设置
* @param timeout 超时时间(毫秒),默认15000ms
* @returns 解析后的音乐URL及相关信息 * @returns 解析后的音乐URL及相关信息
*/ */
export const parseFromGDMusic = async ( export const parseFromGDMusic = async (
id: number, id: number,
data: any, data: any,
quality: string = '320' quality: string = '999',
timeout: number = 15000
): Promise<ParsedMusicResult | null> => { ): Promise<ParsedMusicResult | null> => {
// 创建一个超时Promise
const timeoutPromise = new Promise<null>((_, reject) => {
setTimeout(() => {
reject(new Error('GD音乐台解析超时'));
}, timeout);
});
try { try {
// 使用Promise.race竞争主解析流程和超时
return await Promise.race([
(async () => {
// 处理不同数据结构 // 处理不同数据结构
if (!data) { if (!data) {
console.error('GD音乐台解析:歌曲数据为空'); console.error('GD音乐台解析:歌曲数据为空');
@@ -61,10 +73,9 @@ export const parseFromGDMusic = async (
throw new Error('搜索查询过短'); throw new Error('搜索查询过短');
} }
// 所有可用的音乐源 // 所有可用的音乐源 netease、kuwo、joox、tidal
const allSources = [ const allSources = [
'tencent', 'kugou', 'kuwo', 'migu', 'netease', 'kuwo', 'joox', 'tidal', 'netease'
'joox', 'ytmusic', 'spotify', 'qobuz', 'deezer'
] as MusicSourceType[]; ] as MusicSourceType[];
console.log('GD音乐台开始搜索:', searchQuery); console.log('GD音乐台开始搜索:', searchQuery);
@@ -102,8 +113,15 @@ export const parseFromGDMusic = async (
console.log('GD音乐台所有音源均解析失败'); console.log('GD音乐台所有音源均解析失败');
return null; return null;
} catch (error) { })(),
timeoutPromise
]);
} catch (error: any) {
if (error.message === 'GD音乐台解析超时') {
console.error('GD音乐台解析超时(15秒):', error);
} else {
console.error('GD音乐台解析完全失败:', error); console.error('GD音乐台解析完全失败:', error);
}
return null; return null;
} }
}; };
+14 -14
View File
@@ -31,32 +31,32 @@
<!-- 控制按钮区域 --> <!-- 控制按钮区域 -->
<div class="control-buttons"> <div class="control-buttons">
<button class="control-button previous" @click="handlePrev"> <div class="control-button previous" @click="handlePrev">
<i class="iconfont icon-prev"></i> <i class="iconfont icon-prev"></i>
</button> </div>
<button class="control-button play" @click="playMusicEvent"> <div class="control-button play" @click="playMusicEvent">
<i class="iconfont" :class="play ? 'icon-stop' : 'icon-play'"></i> <i class="iconfont" :class="play ? 'icon-stop' : 'icon-play'"></i>
</button> </div>
<button class="control-button next" @click="handleNext"> <div class="control-button next" @click="handleNext">
<i class="iconfont icon-next"></i> <i class="iconfont icon-next"></i>
</button> </div>
</div> </div>
<!-- 右侧功能按钮 --> <!-- 右侧功能按钮 -->
<div class="function-buttons"> <div class="function-buttons">
<button class="function-button"> <div class="function-button">
<i <i
class="iconfont icon-likefill" class="iconfont icon-likefill"
:class="{ 'like-active': isFavorite }" :class="{ 'like-active': isFavorite }"
@click="toggleFavorite" @click="toggleFavorite"
></i> ></i>
</button> </div>
<n-popover trigger="click" :z-index="99999999" placement="top" :show-arrow="false"> <n-popover trigger="click" :z-index="99999999" placement="top" :show-arrow="false">
<template #trigger> <template #trigger>
<button class="function-button" @click="mute"> <div class="function-button" @click="mute">
<i class="iconfont" :class="getVolumeIcon"></i> <i class="iconfont" :class="getVolumeIcon"></i>
</button> </div>
</template> </template>
<div class="volume-slider-wrapper"> <div class="volume-slider-wrapper">
<n-slider <n-slider
@@ -69,15 +69,15 @@
</n-popover> </n-popover>
<!-- 播放列表按钮 --> <!-- 播放列表按钮 -->
<button v-if="!component" class="function-button" @click="togglePlaylist"> <div v-if="!component" class="function-button" @click="togglePlaylist">
<i class="iconfont icon-list"></i> <i class="iconfont icon-list"></i>
</button> </div>
</div> </div>
<!-- 关闭按钮 --> <!-- 关闭按钮 -->
<button v-if="!component" class="close-button" @click="handleClose"> <div v-if="!component" class="close-button" @click="handleClose">
<i class="iconfont ri-close-line"></i> <i class="iconfont ri-close-line"></i>
</button> </div>
</div> </div>
<!-- 进度条 --> <!-- 进度条 -->
@@ -39,6 +39,9 @@
lazy lazy
preview-disabled preview-disabled
/> />
<div v-if="playMusic?.playLoading" class="loading-overlay">
<i class="ri-loader-4-line loading-icon"></i>
</div>
<div class="hover-arrow"> <div class="hover-arrow">
<div class="hover-content"> <div class="hover-content">
<!-- <i class="ri-arrow-up-s-line text-3xl" :class="{ 'ri-arrow-down-s-line': musicFullVisible }"></i> --> <!-- <i class="ri-arrow-up-s-line text-3xl" :class="{ 'ri-arrow-down-s-line': musicFullVisible }"></i> -->
@@ -758,4 +761,25 @@ const handleDeleteSong = (song: SongResult) => {
} }
} }
} }
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading-overlay {
@apply absolute inset-0 flex items-center justify-center rounded-2xl;
background-color: rgba(0, 0, 0, 0.5);
z-index: 2;
}
.loading-icon {
font-size: 24px;
color: white;
animation: spin 1s linear infinite;
}
</style> </style>
+35 -1
View File
@@ -34,6 +34,7 @@
:class="{ 'only-cover': config.hideLyrics }" :class="{ 'only-cover': config.hideLyrics }"
:style="{ color: textColors.theme === 'dark' ? '#000000' : '#ffffff' }" :style="{ color: textColors.theme === 'dark' ? '#000000' : '#ffffff' }"
> >
<div class="img-container relative">
<n-image <n-image
ref="PicImgRef" ref="PicImgRef"
:src="getImgUrl(playMusic?.picUrl, '500y500')" :src="getImgUrl(playMusic?.picUrl, '500y500')"
@@ -41,6 +42,10 @@
lazy lazy
preview-disabled preview-disabled
/> />
<div v-if="playMusic?.playLoading" class="loading-overlay">
<i class="ri-loader-4-line loading-icon"></i>
</div>
</div>
<div class="music-info"> <div class="music-info">
<div class="music-content-name">{{ playMusic.name }}</div> <div class="music-content-name">{{ playMusic.name }}</div>
<div class="music-content-singer"> <div class="music-content-singer">
@@ -549,10 +554,14 @@ defineExpose({
max-width: none; max-width: none;
max-height: none; max-height: none;
.img { .img-container {
@apply w-[50vh] h-[50vh] mb-8; @apply w-[50vh] h-[50vh] mb-8;
} }
.img {
@apply w-full h-full;
}
.music-info { .music-info {
@apply text-center w-[600px]; @apply text-center w-[600px];
@@ -568,6 +577,10 @@ defineExpose({
} }
} }
.img-container {
@apply relative w-full h-full;
}
.img { .img {
@apply rounded-xl w-full h-full shadow-2xl transition-all duration-300; @apply rounded-xl w-full h-full shadow-2xl transition-all duration-300;
} }
@@ -763,4 +776,25 @@ defineExpose({
} }
} }
} }
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading-overlay {
@apply absolute inset-0 flex items-center justify-center rounded-xl;
background-color: rgba(0, 0, 0, 0.5);
z-index: 2;
}
.loading-icon {
font-size: 48px;
color: white;
animation: spin 1s linear infinite;
}
</style> </style>
+82 -97
View File
@@ -189,17 +189,26 @@ export const loadLrc = async (id: string | number): Promise<ILyric> => {
}; };
const getSongDetail = async (playMusic: SongResult) => { const getSongDetail = async (playMusic: SongResult) => {
playMusic.playLoading = true; // playMusic.playLoading 在 handlePlayMusic 中已设置,这里不再设置
if (playMusic.source === 'bilibili') { if (playMusic.source === 'bilibili') {
console.log('处理B站音频详情'); console.log('处理B站音频详情');
const { backgroundColor, primaryColor } = try {
playMusic.backgroundColor && playMusic.primaryColor // 如果需要获取URL
? playMusic if (!playMusic.playMusicUrl && playMusic.bilibiliData) {
: await getImageLinearBackground(getImgUrl(playMusic?.picUrl, '30y30')); playMusic.playMusicUrl = await getBilibiliAudioUrl(
playMusic.bilibiliData.bvid,
playMusic.bilibiliData.cid
);
}
playMusic.playLoading = false; playMusic.playLoading = false;
return { ...playMusic, backgroundColor, primaryColor } as SongResult; return { ...playMusic} as SongResult;
} catch (error) {
console.error('获取B站音频详情失败:', error);
playMusic.playLoading = false;
throw error;
}
} }
if (playMusic.expiredAt && playMusic.expiredAt < Date.now()) { if (playMusic.expiredAt && playMusic.expiredAt < Date.now()) {
@@ -207,6 +216,7 @@ const getSongDetail = async (playMusic: SongResult) => {
playMusic.playMusicUrl = undefined; playMusic.playMusicUrl = undefined;
} }
try {
const playMusicUrl = playMusic.playMusicUrl || (await getSongUrl(playMusic.id, playMusic)); const playMusicUrl = playMusic.playMusicUrl || (await getSongUrl(playMusic.id, playMusic));
playMusic.createdAt = Date.now(); playMusic.createdAt = Date.now();
// 半小时后过期 // 半小时后过期
@@ -218,6 +228,11 @@ const getSongDetail = async (playMusic: SongResult) => {
playMusic.playLoading = false; playMusic.playLoading = false;
return { ...playMusic, playMusicUrl, backgroundColor, primaryColor } as SongResult; return { ...playMusic, playMusicUrl, backgroundColor, primaryColor } as SongResult;
} catch (error) {
console.error('获取音频URL失败:', error);
playMusic.playLoading = false;
throw error;
}
}; };
const preloadNextSong = (nextSongUrl: string) => { const preloadNextSong = (nextSongUrl: string) => {
@@ -389,71 +404,72 @@ export const usePlayerStore = defineStore('player', () => {
const currentPlayListIndex = computed(() => playListIndex.value); const currentPlayListIndex = computed(() => playListIndex.value);
const handlePlayMusic = async (music: SongResult, isPlay: boolean = true) => { const handlePlayMusic = async (music: SongResult, isPlay: boolean = true) => {
// 处理B站视频,确保URL有效 const currentSound = audioService.getCurrentSound();
if (music.source === 'bilibili' && music.bilibiliData) { if (currentSound) {
try { console.log('主动停止并卸载当前音频实例');
console.log('处理B站视频,检查URL有效性'); currentSound.stop();
// 清除之前的URL,强制重新获取 currentSound.unload();
music.playMusicUrl = undefined;
// 重新获取B站视频URL
if (music.bilibiliData.bvid && music.bilibiliData.cid) {
music.playMusicUrl = await getBilibiliAudioUrl(
music.bilibiliData.bvid,
music.bilibiliData.cid
);
console.log('获取B站URL成功:', music.playMusicUrl);
}
} catch (error) {
console.error('获取B站音频URL失败:', error);
message.error(i18n.global.t('player.playFailed'));
return false; // 返回失败状态
}
} }
// 先切换歌曲数据,更新播放状态
// 加载歌词
await loadLrcAsync(music);
const originalMusic = { ...music };
// 获取背景色
const { backgroundColor, primaryColor } =
music.backgroundColor && music.primaryColor
? music
: await getImageLinearBackground(getImgUrl(music?.picUrl, '30y30'));
music.backgroundColor = backgroundColor;
music.primaryColor = primaryColor;
music.playLoading = true; // 设置加载状态
playMusic.value = music;
try { // 更新播放相关状态
const updatedPlayMusic = await getSongDetail(music);
playMusic.value = updatedPlayMusic;
playMusicUrl.value = updatedPlayMusic.playMusicUrl as string;
play.value = isPlay; play.value = isPlay;
localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value)); // 更新标题
localStorage.setItem('currentPlayMusicUrl', playMusicUrl.value); let title = music.name;
localStorage.setItem('isPlaying', play.value.toString()); if (music.source === 'netease' && music?.song?.artists) {
title += ` - ${music.song.artists.reduce(
let title = updatedPlayMusic.name;
if (updatedPlayMusic.source === 'netease' && updatedPlayMusic?.song?.artists) {
title += ` - ${updatedPlayMusic.song.artists.reduce(
(prev: string, curr: any) => `${prev}${curr.name}/`, (prev: string, curr: any) => `${prev}${curr.name}/`,
'' ''
)}`; )}`;
} else if (updatedPlayMusic.source === 'bilibili' && updatedPlayMusic?.song?.ar?.[0]) { } else if (music.source === 'bilibili' && music?.song?.ar?.[0]) {
title += ` - ${updatedPlayMusic.song.ar[0].name}`; title += ` - ${music.song.ar[0].name}`;
} }
document.title = 'AlgerMusic - ' + title;
document.title = title; try {
loadLrcAsync(playMusic.value); // 添加到历史记录
musicHistory.addMusic(music);
musicHistory.addMusic(playMusic.value); // 查找歌曲在播放列表中的索引
// 找到歌曲在播放列表中的索引,如果是通过 nextPlay/prevPlay 调用的,不会更新 playListIndex
const songIndex = playList.value.findIndex( const songIndex = playList.value.findIndex(
(item: SongResult) => item.id === music.id && item.source === music.source (item: SongResult) => item.id === music.id && item.source === music.source
); );
// 只有在 songIndex 有效,并且与当前 playListIndex 不同时才更新 // 只有在 songIndex 有效,并且与当前 playListIndex 不同时才更新
// 这样可以避免与 nextPlay/prevPlay 中的索引更新冲突
if (songIndex !== -1 && songIndex !== playListIndex.value) { if (songIndex !== -1 && songIndex !== playListIndex.value) {
console.log('歌曲索引不匹配,更新为:', songIndex); console.log('歌曲索引不匹配,更新为:', songIndex);
playListIndex.value = songIndex; playListIndex.value = songIndex;
} }
// 获取歌曲详情,包括URL
const updatedPlayMusic = await getSongDetail(originalMusic);
playMusic.value = updatedPlayMusic;
playMusicUrl.value = updatedPlayMusic.playMusicUrl as string;
// 保存到本地存储
localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value));
localStorage.setItem('currentPlayMusicUrl', playMusicUrl.value);
localStorage.setItem('isPlaying', play.value.toString());
// 无论如何都预加载更多歌曲 // 无论如何都预加载更多歌曲
if (songIndex !== -1) { if (songIndex !== -1) {
fetchSongs(playList.value, songIndex + 1, songIndex + 3); setTimeout(() => {
fetchSongs(playList.value, songIndex + 1, songIndex + 2);
}, 3000);
} else { } else {
console.warn('当前歌曲未在播放列表中找到'); console.warn('当前歌曲未在播放列表中找到');
} }
@@ -461,7 +477,7 @@ export const usePlayerStore = defineStore('player', () => {
// 使用标记防止循环调用 // 使用标记防止循环调用
let playInProgress = false; let playInProgress = false;
// 直接调用 playAudio 方法播放音频,不需要依赖外部监听 // 直接调用 playAudio 方法播放音频
try { try {
if (playInProgress) { if (playInProgress) {
console.warn('播放操作正在进行中,避免重复调用'); console.warn('播放操作正在进行中,避免重复调用');
@@ -469,8 +485,6 @@ export const usePlayerStore = defineStore('player', () => {
} }
playInProgress = true; playInProgress = true;
// 因为调用 playAudio 前我们已经设置了 play.value,所以不需要额外传递 shouldPlay 参数
const result = await playAudio(); const result = await playAudio();
playInProgress = false; playInProgress = false;
@@ -483,6 +497,10 @@ export const usePlayerStore = defineStore('player', () => {
} catch (error) { } catch (error) {
console.error('处理播放音乐失败:', error); console.error('处理播放音乐失败:', error);
message.error(i18n.global.t('player.playFailed')); message.error(i18n.global.t('player.playFailed'));
// 出现错误时,更新加载状态
if (playMusic.value) {
playMusic.value.playLoading = false;
}
return false; return false;
} }
}; };
@@ -717,20 +735,13 @@ export const usePlayerStore = defineStore('player', () => {
} }
}; };
// 修改nextPlay方法,改进播放失败的处理逻辑 // 修改nextPlay方法,改进播放逻辑
const nextPlay = async () => { const nextPlay = async () => {
// 静态标志,防止多次调用造成递归
if ((nextPlay as any).isRunning) {
console.log('下一首播放正在执行中,忽略重复调用');
return;
}
try { try {
(nextPlay as any).isRunning = true;
if (playList.value.length === 0) { if (playList.value.length === 0) {
play.value = true; play.value = true;
(nextPlay as any).isRunning = false;
return; return;
} }
@@ -739,7 +750,6 @@ export const usePlayerStore = defineStore('player', () => {
sleepTimer.value.type === SleepTimerType.PLAYLIST_END) { sleepTimer.value.type === SleepTimerType.PLAYLIST_END) {
// 已是最后一首且为顺序播放模式,触发停止 // 已是最后一首且为顺序播放模式,触发停止
stopPlayback(); stopPlayback();
(nextPlay as any).isRunning = false;
return; return;
} }
@@ -761,29 +771,23 @@ export const usePlayerStore = defineStore('player', () => {
nowPlayListIndex = (playListIndex.value + 1) % playList.value.length; nowPlayListIndex = (playListIndex.value + 1) % playList.value.length;
} }
// 获取下一首歌曲
let nextSong = { ...playList.value[nowPlayListIndex] };
// 记录尝试播放过的索引,防止无限循环 // 记录尝试播放过的索引,防止无限循环
const attemptedIndices = new Set<number>(); const attemptedIndices = new Set<number>();
attemptedIndices.add(nowPlayListIndex); attemptedIndices.add(nowPlayListIndex);
// 更新当前播放索引 // 更新当前播放索引
playListIndex.value = nowPlayListIndex; playListIndex.value = nowPlayListIndex;
// 获取下一首歌曲 // 尝试播放
let nextSong = playList.value[nowPlayListIndex];
let success = false; let success = false;
let retryCount = 0; let retryCount = 0;
const maxRetries = Math.min(3, playList.value.length); const maxRetries = Math.min(3, playList.value.length);
// 尝试播放,最多尝试maxRetries次 // 尝试播放,最多尝试maxRetries次
while (!success && retryCount < maxRetries) { while (!success && retryCount < maxRetries) {
// 如果是B站视频,确保重新获取URL
if (nextSong.source === 'bilibili' && nextSong.bilibiliData) {
// 清除之前的URL,确保重新获取
nextSong.playMusicUrl = undefined;
console.log(`尝试播放B站视频 (尝试 ${retryCount + 1}/${maxRetries})`);
}
// 尝试播放,并明确传递应该播放的状态
success = await handlePlayMusic(nextSong, shouldPlayNext); success = await handlePlayMusic(nextSong, shouldPlayNext);
if (!success) { if (!success) {
@@ -798,7 +802,6 @@ export const usePlayerStore = defineStore('player', () => {
if (newPlayList.length > 0) { if (newPlayList.length > 0) {
// 更新播放列表,但保持当前索引不变 // 更新播放列表,但保持当前索引不变
// 这是关键修改,防止索引重置到-1
const keepCurrentIndexPosition = true; const keepCurrentIndexPosition = true;
setPlayList(newPlayList, keepCurrentIndexPosition); setPlayList(newPlayList, keepCurrentIndexPosition);
@@ -829,7 +832,7 @@ export const usePlayerStore = defineStore('player', () => {
attemptedIndices.add(nowPlayListIndex); attemptedIndices.add(nowPlayListIndex);
if (newPlayList[nowPlayListIndex]) { if (newPlayList[nowPlayListIndex]) {
nextSong = newPlayList[nowPlayListIndex]; nextSong = { ...newPlayList[nowPlayListIndex] };
retryCount = 0; // 重置重试计数器,为新歌曲准备 retryCount = 0; // 重置重试计数器,为新歌曲准备
} else { } else {
// 处理索引无效的情况 // 处理索引无效的情况
@@ -857,25 +860,17 @@ export const usePlayerStore = defineStore('player', () => {
} }
} catch (error) { } catch (error) {
console.error('切换下一首出错:', error); console.error('切换下一首出错:', error);
} finally {
(nextPlay as any).isRunning = false;
} }
}; };
// 修改 prevPlay 方法,使用与 nextPlay 相似的逻辑改进 // 修改 prevPlay 方法,使用与 nextPlay 相似的逻辑改进
const prevPlay = async () => { const prevPlay = async () => {
// 静态标志,防止多次调用造成递归
if ((prevPlay as any).isRunning) {
console.log('上一首播放正在执行中,忽略重复调用');
return;
}
try { try {
(prevPlay as any).isRunning = true;
if (playList.value.length === 0) { if (playList.value.length === 0) {
play.value = true; play.value = true;
(prevPlay as any).isRunning = false;
return; return;
} }
@@ -884,22 +879,17 @@ export const usePlayerStore = defineStore('player', () => {
const nowPlayListIndex = const nowPlayListIndex =
(playListIndex.value - 1 + playList.value.length) % playList.value.length; (playListIndex.value - 1 + playList.value.length) % playList.value.length;
// 获取上一首歌曲
const prevSong = { ...playList.value[nowPlayListIndex] };
// 重要:首先更新当前播放索引 // 重要:首先更新当前播放索引
playListIndex.value = nowPlayListIndex; playListIndex.value = nowPlayListIndex;
// 获取上一首歌曲 // 尝试播放
const prevSong = playList.value[nowPlayListIndex];
let success = false; let success = false;
let retryCount = 0; let retryCount = 0;
const maxRetries = 2; const maxRetries = 2;
// 如果是B站视频,确保重新获取URL
if (prevSong.source === 'bilibili' && prevSong.bilibiliData) {
// 清除之前的URL,确保重新获取
prevSong.playMusicUrl = undefined;
console.log('上一首是B站视频,已清除URL强制重新获取');
}
// 尝试播放,最多尝试maxRetries次 // 尝试播放,最多尝试maxRetries次
while (!success && retryCount < maxRetries) { while (!success && retryCount < maxRetries) {
success = await handlePlayMusic(prevSong); success = await handlePlayMusic(prevSong);
@@ -932,7 +922,6 @@ export const usePlayerStore = defineStore('player', () => {
// 延迟一点时间再尝试,避免可能的无限循环 // 延迟一点时间再尝试,避免可能的无限循环
setTimeout(() => { setTimeout(() => {
(prevPlay as any).isRunning = false;
prevPlay(); // 递归调用,尝试再上一首 prevPlay(); // 递归调用,尝试再上一首
}, 300); }, 300);
return; return;
@@ -945,9 +934,7 @@ export const usePlayerStore = defineStore('player', () => {
} }
} }
if (success) { if (!success) {
await fetchSongs(playList.value, playListIndex.value - 3, nowPlayListIndex);
} else {
console.error('所有尝试都失败,无法播放上一首歌曲'); console.error('所有尝试都失败,无法播放上一首歌曲');
// 如果尝试了所有可能的歌曲仍然失败,恢复到原始索引 // 如果尝试了所有可能的歌曲仍然失败,恢复到原始索引
playListIndex.value = currentIndex; playListIndex.value = currentIndex;
@@ -956,8 +943,6 @@ export const usePlayerStore = defineStore('player', () => {
} }
} catch (error) { } catch (error) {
console.error('切换上一首出错:', error); console.error('切换上一首出错:', error);
} finally {
(prevPlay as any).isRunning = false;
} }
}; };