From 080af14be8de7f3b0d72221a87d00fb077f83ae6 Mon Sep 17 00:00:00 2001 From: zhangyibo95 <690096405@qq.com> Date: Mon, 13 Jul 2026 14:50:21 +0800 Subject: [PATCH] =?UTF-8?q?update:=E8=AF=AD=E9=9F=B3=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=8C=E4=BF=AE=E5=A4=8D=E5=A4=A7=E4=B9=B1?= =?UTF-8?q?=E6=96=97=E6=9C=AA=E9=87=8D=E8=BF=9E=E6=AF=94=E8=B5=9B=E6=9C=8D?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 1 + src/audioManager.js | 554 ++++++++++-------- src/components/BackToGame.vue | 1 - src/components/Container.vue | 120 +--- src/pages/first-try.vue | 1 + src/pages/index.vue | 5 - src/pages/match-page.vue | 2 + src/pages/melee-battle.vue | 13 +- src/pages/practise-one.vue | 1 + src/pages/practise-two.vue | 1 + src/pages/ranking.vue | 1 - .../team-battle/components/BackToGame.vue | 1 - .../team-battle/components/Container.vue | 57 +- src/pages/team-battle/index.vue | 18 +- src/pages/user.vue | 1 - src/utils/matchReturn.js | 10 - 16 files changed, 340 insertions(+), 447 deletions(-) diff --git a/src/App.vue b/src/App.vue index 601fa03..608b1ec 100644 --- a/src/App.vue +++ b/src/App.vue @@ -108,6 +108,7 @@ } onShow(() => { + void audioManager.warmButton(); uni.$on("update-user", emitUpdateUser); uni.$on("update-online", emitUpdateOnline); uni.$on("session-kicked-out", onSessionKickedOut); diff --git a/src/audioManager.js b/src/audioManager.js index 156553c..fc7f899 100644 --- a/src/audioManager.js +++ b/src/audioManager.js @@ -95,6 +95,28 @@ export const audioFils = { "https://static.shelingxingqiu.com/shootaudio/%E5%91%BD%E4%B8%AD.mp3" }; +const AUDIO_LOAD_TIMEOUT_MS = 5000; +const AUDIO_WARM_CONCURRENCY = 3; +const AUDIO_WARM_RETRIES = 1; +const AUDIO_WARM_PRIORITY_KEYS = [ + "点击按钮", + "比赛开始", + "练习开始", + "请开始射击", + "轮到你了", + "比赛结束", + "射击无效", + "中场休息", + "下半场开始", + "决金箭轮", + "请蓝方射箭", + "请红方射箭", + "距离合格", + "距离不足", + "未上靶", + "X环", +]; + // 版本控制日志函数 function debugLog(...args) { // 获取当前环境信息 @@ -111,18 +133,15 @@ class AudioManager { constructor() { this.audioMap = new Map(); this.currentPlayingKey = null; - this.maxRetries = 3; - // 多轮统一重试:最多重试的轮次与每轮间隔 - this.maxRetryRounds = 10; - this.retryRoundIntervalMs = 1500; // 显式授权播放标记,防止 iOS 在设置 src 后误播 this.allowPlayMap = new Map(); - // 串行加载相关属性 - this.audioKeys = []; - this.currentLoadingIndex = 0; + // 后台预热与单条加载状态 this.isLoading = false; - this.loadingPromise = null; + this.loadingKeyPromises = new Map(); + this.pendingPlayKeys = new Set(); + this.cacheWritePromises = new Map(); + this.warmAllPromise = null; // 连续播放队列相关属性 this.sequenceQueue = []; @@ -141,19 +160,14 @@ class AudioManager { // 静音开关 this.isMuted = false; this.pendingPlayKey = null; - // 新增:就绪状态映射 this.readyMap = new Map(); - // 新增:首轮失败的音频集合与重试阶段标识 - this.failedLoadKeys = new Set(); - // 加载代数,用于 reloadAll 时作废旧的加载循环 + // 加载代数,用于 reloadAll 时作废旧的异步加载 this.loadGeneration = 0; // 本地路径缓存 Map: { url: localPath } this.localFileCache = uni.getStorageSync("audio_local_files") || {}; // 启动时自动清理过期的缓存文件(URL 已不在 audioFils 中的文件) this.cleanObsoleteCache(); this.bindAudioInterruptionEvents(); - - this.initAudios(); } bindAudioInterruptionEvents() { @@ -217,128 +231,122 @@ class AudioManager { } } - // 初始化音频(两阶段:首轮串行加载全部,次轮仅串行加载失败项一次) - initAudios() { - if (this.isLoading) { - debugLog("音频正在加载中,跳过重复初始化"); - return this.loadingPromise; - } - debugLog("开始串行加载音频..."); - this.isLoading = true; - this.audioKeys = Object.keys(audioFils); - this.currentLoadingIndex = 0; - this.failedLoadKeys.clear(); - - // 增加代数,使得旧的加载循环失效 - this.loadGeneration = (this.loadGeneration || 0) + 1; - const currentGen = this.loadGeneration; - - this.loadingPromise = new Promise((resolve) => { - const finalize = () => { - if (currentGen !== this.loadGeneration) return; - const runRounds = (round) => { - if (currentGen !== this.loadGeneration) return; - // 达到最大轮次或没有失败项,收尾 - if (this.failedLoadKeys.size === 0 || round > this.maxRetryRounds) { - this.isLoading = false; - resolve(); - return; - } - - const retryKeys = Array.from(this.failedLoadKeys); - this.failedLoadKeys.clear(); - debugLog(`开始第 ${round} 轮串行加载,共 ${retryKeys.length} 个`); - - this.loadKeysSequentially( - retryKeys, - () => { - if (currentGen !== this.loadGeneration) return; - // 如仍有失败项,继续下一轮;否则结束 - if (this.failedLoadKeys.size > 0 && round < this.maxRetryRounds) { - setTimeout( - () => runRounds(round + 1), - this.retryRoundIntervalMs - ); - } else { - this.isLoading = false; - resolve(); - } - }, - currentGen - ); - }; - - // 启动第 1 轮重试(如有失败项) - runRounds(1); - }; - this.loadNextAudio(finalize, currentGen); - }); - return this.loadingPromise; + getWarmupKeys() { + return Array.from( + new Set([...AUDIO_WARM_PRIORITY_KEYS, ...Object.keys(audioFils)]) + ); } - // 按自定义列表串行加载音频(避免并发过多) - loadKeysSequentially(keys, onComplete, gen) { - if (gen !== undefined && gen !== this.loadGeneration) return; - let idx = 0; - const list = Array.from(keys); - const next = () => { - if (gen !== undefined && gen !== this.loadGeneration) return; - if (idx >= list.length) { - if (onComplete) onComplete(); - return; - } - const k = list[idx++]; + ensureAudio(key) { + if (!audioFils[key]) return Promise.resolve(false); + if (this.readyMap.get(key) && this.audioMap.has(key)) { + return Promise.resolve(true); + } - // 已就绪的音频不再重载,避免把 ready 状态重置为 false - if (this.readyMap.get(k)) { - setTimeout(next, 50); - return; - } + const loadingPromise = this.loadingKeyPromises.get(key); + if (loadingPromise) return loadingPromise; - // 未就绪:已存在则重载;不存在则创建 - if (this.audioMap.has(k)) { - this.retryLoadAudio(k); - setTimeout(next, 100); - } else { - this.createAudio(k, () => { - setTimeout(next, 100); + let promise; + promise = new Promise((resolve) => { + try { + this.createAudio(key, () => { + resolve(this.readyMap.get(key) === true && this.audioMap.has(key)); }); - return; // createAudio 内部会触发 next + } catch (error) { + debugLog(`音频 ${key} 创建失败`, error); + resolve(false); + } + }).finally(() => { + if (this.loadingKeyPromises.get(key) === promise) { + this.loadingKeyPromises.delete(key); + } + }); + this.loadingKeyPromises.set(key, promise); + return promise; + } + + async warmKeys( + keys, + { concurrency = AUDIO_WARM_CONCURRENCY, retries = AUDIO_WARM_RETRIES } = {} + ) { + const queue = Array.from(new Set(keys)).filter((key) => !!audioFils[key]); + if (!queue.length) return; + + let index = 0; + const worker = async () => { + while (index < queue.length) { + const key = queue[index++]; + let ready = await this.ensureAudio(key); + for (let attempt = 0; !ready && attempt < retries; attempt += 1) { + ready = await this.retryLoadAudio(key); + } } }; - next(); + + const workerCount = Math.min(Math.max(1, concurrency), queue.length); + await Promise.all(Array.from({ length: workerCount }, () => worker())); } - // 串行加载下一个音频(首轮) - loadNextAudio(onComplete, gen) { - if (gen !== undefined && gen !== this.loadGeneration) return; - if (this.currentLoadingIndex >= this.audioKeys.length) { - debugLog("首轮加载遍历完成", this.currentLoadingIndex); - if (onComplete) onComplete(); + warmButton() { + return this.warmKeys(["点击按钮"], { concurrency: 1, retries: 1 }); + } + + warmAll() { + if (this.warmAllPromise) return this.warmAllPromise; + + debugLog("开始后台分级预热全部语音"); + this.isLoading = true; + this.warmAllPromise = this.warmKeys(this.getWarmupKeys()) + .catch((error) => { + debugLog("后台语音预热异常", error); + }) + .finally(() => { + this.isLoading = false; + debugLog("后台语音预热结束", this.getLoadProgress()); + }); + return this.warmAllPromise; + } + + // 保留旧入口兼容测试页或其他历史调用,实际行为改为非阻塞后台预热。 + initAudios() { + return this.warmAll(); + } + + // 创建单个音频实例:缓存命中优先使用,未命中直接使用 CDN。 + createAudio(key, callback) { + const src = audioFils[key]; + const generation = this.loadGeneration; + let completed = false; + + const complete = () => { + if (completed) return; + completed = true; + if (callback) callback(); + }; + + if (!src) { + complete(); return; } - const key = this.audioKeys[this.currentLoadingIndex]; - debugLog( - `开始加载音频 ${this.currentLoadingIndex + 1}/${ - this.audioKeys.length - }: ${key}` - ); - this.createAudio(key, () => { - setTimeout(() => { - this.loadNextAudio(onComplete, gen); - }, 100); - }); - } - // 创建单个音频实例(支持本地缓存) - createAudio(key, callback) { - this.currentLoadingIndex++; - const src = audioFils[key]; + const existingAudio = this.audioMap.get(key); + if (existingAudio && !this.readyMap.get(key)) { + try { + existingAudio.destroy(); + } catch (_) {} + this.audioMap.delete(key); + } + + const isCurrentGeneration = () => generation === this.loadGeneration; + + const setupAudio = (realSrc, resumeAfterReady = false) => { + if (!isCurrentGeneration()) { + complete(); + return; + } - const setupAudio = (realSrc) => { const audio = uni.createInnerAudioContext(); audio.autoplay = false; - audio.src = realSrc; try { if (typeof audio.volume === "number") { audio.volume = this.isMuted ? 0 : 1; @@ -355,63 +363,99 @@ class AudioManager { } }); - const loadTimeout = setTimeout(() => { - debugLog(`音频 ${key} 加载超时`); - this.recordLoadFailure(key); + let loadTimeout = null; + let readyHandled = false; + const clearLoadTimeout = () => { + if (!loadTimeout) return; + clearTimeout(loadTimeout); + loadTimeout = null; + }; + + const handleLoadFailure = (message) => { + if (this.audioMap.get(key) !== audio) return; + clearLoadTimeout(); + debugLog(`音频 ${key} ${message}`); + + const isLocalCache = + realSrc !== src && this.localFileCache[src] === realSrc; + const shouldResume = + this.allowPlayMap.get(key) === true || this.currentPlayingKey === key; + const shouldResumeDirectly = shouldResume && completed; + + this.clearPlayWatchdog(key); + this.allowPlayMap.set(key, false); + if (this.currentPlayingKey === key) { + this.currentPlayingKey = null; + } + if (this.lastPlayKey === key) { + this.lastPlayKey = null; + this.lastPlayAt = 0; + } + this.readyMap.set(key, false); this.audioMap.delete(key); try { audio.destroy(); } catch (_) {} + + if (isLocalCache && isCurrentGeneration()) { + debugLog(`本地缓存失效,立即切换 CDN: ${key}`); + delete this.localFileCache[src]; + uni.setStorageSync("audio_local_files", this.localFileCache); + uni.removeSavedFile({ filePath: realSrc }); + setupAudio(src, shouldResumeDirectly); + void this.cacheRemoteFile(src, key); + return; + } + this.finishPlayback(key, { advanceSequence: true, emitEnded: true, }); - if (callback) callback(); - }, 10000); + complete(); + }; + + loadTimeout = setTimeout(() => { + handleLoadFailure("加载超时"); + }, AUDIO_LOAD_TIMEOUT_MS); audio.onCanplay(() => { + if (this.audioMap.get(key) !== audio) return; + if (readyHandled) return; + readyHandled = true; + if (!isCurrentGeneration()) { + clearLoadTimeout(); + this.audioMap.delete(key); + try { + audio.destroy(); + } catch (_) {} + complete(); + return; + } if (!this.allowPlayMap.get(key)) { try { audio.pause(); } catch (_) {} } - clearTimeout(loadTimeout); + clearLoadTimeout(); this.readyMap.set(key, true); - this.failedLoadKeys.delete(key); - // debugLog(`音频 ${key} 已加载完成`, this.getLoadProgress()); uni.$emit("audioLoaded", key); const loadedAudioKeys = uni.getStorageSync("loadedAudioKeys") || {}; loadedAudioKeys[key] = true; uni.setStorageSync("loadedAudioKeys", loadedAudioKeys); - if (callback) callback(); + complete(); + + if (resumeAfterReady) { + setTimeout(() => { + const isSequenceCurrent = + this.isSequenceRunning && + this.sequenceQueue[this.sequenceIndex] === key; + if (isSequenceCurrent) this._playSingle(key, false); + }, 0); + } }); audio.onError((res) => { - clearTimeout(loadTimeout); - debugLog(`音频 ${key} 加载失败:`, res.errMsg); - // 如果是本地文件加载失败,可能是文件损坏,清除缓存以便下次重新下载 - if (realSrc !== src && this.localFileCache[src] === realSrc) { - debugLog(`本地缓存失效,移除记录: ${key}`); - delete this.localFileCache[src]; - uni.setStorageSync("audio_local_files", this.localFileCache); - // 移除文件 - uni.removeSavedFile({ filePath: realSrc }); - } - this.recordLoadFailure(key); - this.audioMap.delete(key); - try { - audio.destroy(); - } catch (_) {} - this.finishPlayback(key, { - advanceSequence: true, - emitEnded: true, - }); - if (this.readyMap.get(key)) { - // 这里不要去除,不然检查进度的时候由于没有重新加载而进度卡住,等播放失败的时候会重新加载 - // this.readyMap.set(key, false); - } else { - if (callback) callback(); - } + handleLoadFailure(`加载失败:${res.errMsg || "unknown"}`); }); audio.onEnded(() => { @@ -426,58 +470,78 @@ class AudioManager { }); this.audioMap.set(key, audio); + audio.src = realSrc; }; // 检查是否有可用的本地缓存 this.checkLocalFile(src).then((localPath) => { + if (!isCurrentGeneration()) { + complete(); + return; + } if (localPath) { debugLog(`命中本地缓存: ${key}`); setupAudio(localPath); } else { - // 下载并尝试保存 - uni.downloadFile({ - url: src, - timeout: 20000, - success: (res) => { - if (res.tempFilePath) { - // 尝试保存文件到本地存储(持久化) - uni.getFileSystemManager().saveFile({ - tempFilePath: res.tempFilePath, - success: (saveRes) => { - const savedPath = saveRes.savedFilePath; - this.localFileCache[src] = savedPath; - uni.setStorageSync("audio_local_files", this.localFileCache); - debugLog(`音频已缓存到本地: ${key}`); - setupAudio(savedPath); - }, - fail: (err) => { - debugLog( - `保存音频失败(可能空间不足),使用临时文件: ${key}`, - err - ); - setupAudio(res.tempFilePath); - }, - }); - } else { - this.recordLoadFailure(key); - this.finishPlayback(key, { - advanceSequence: true, - emitEnded: true, - }); - if (callback) callback(); - } - }, - fail: () => { - this.recordLoadFailure(key); - this.finishPlayback(key, { - advanceSequence: true, - emitEnded: true, - }); - if (callback) callback(); - }, - }); + setupAudio(src); + void this.cacheRemoteFile(src, key); + } + }).catch(() => { + setupAudio(src); + void this.cacheRemoteFile(src, key); + }); + } + + cacheRemoteFile(url, key) { + if (this.localFileCache[url]) return Promise.resolve(true); + const currentPromise = this.cacheWritePromises.get(url); + if (currentPromise) return currentPromise; + + let promise; + promise = new Promise((resolve) => { + if (typeof uni.downloadFile !== "function") { + resolve(false); + return; + } + + uni.downloadFile({ + url, + timeout: 20000, + success: (res) => { + if (!res.tempFilePath) { + resolve(false); + return; + } + + let fileSystemManager; + try { + fileSystemManager = uni.getFileSystemManager(); + } catch (_) { + resolve(false); + return; + } + + fileSystemManager.saveFile({ + tempFilePath: res.tempFilePath, + success: (saveRes) => { + this.localFileCache[url] = saveRes.savedFilePath; + uni.setStorageSync("audio_local_files", this.localFileCache); + debugLog(`音频已后台缓存到本地: ${key}`); + resolve(true); + }, + fail: () => resolve(false), + }); + }, + fail: () => resolve(false), + }); + }).finally(() => { + if (this.cacheWritePromises.get(url) === promise) { + this.cacheWritePromises.delete(url); } }); + + this.cacheWritePromises.set(url, promise); + return promise; } // 检查本地文件是否有效 @@ -504,11 +568,6 @@ class AudioManager { }); } - // 新增:记录失败(首轮与次轮都会用到) - recordLoadFailure(key) { - this.failedLoadKeys.add(key); - } - clearPlayWatchdog(key) { const timer = this.playWatchdogTimers.get(key); if (timer) { @@ -612,19 +671,14 @@ class AudioManager { } reloadAudioKey(key) { - const audio = this.audioMap.get(key); - if (audio) { - try { - audio.destroy(); - } catch (_) {} - this.audioMap.delete(key); - } - this.readyMap.set(key, false); - this.retryLoadAudio(key); + return this.retryLoadAudio(key); } // 重新加载音频 retryLoadAudio(key) { + const loadingPromise = this.loadingKeyPromises.get(key); + if (loadingPromise) return loadingPromise; + this.clearPlayWatchdog(key); const oldAudio = this.audioMap.get(key); if (oldAudio) { @@ -632,7 +686,9 @@ class AudioManager { oldAudio.destroy(); } catch (_) {} } - this.createAudio(key); + this.audioMap.delete(key); + this.readyMap.set(key, false); + return this.ensureAudio(key); } // 播放指定音频或音频数组(数组则按顺序连续播放) @@ -695,6 +751,32 @@ class AudioManager { } } + waitForAudioAndPlay(key) { + if (this.pendingPlayKeys.has(key)) return; + this.pendingPlayKeys.add(key); + + this.ensureAudio(key) + .then((ready) => { + const isSequenceCurrent = + this.isSequenceRunning && this.sequenceQueue[this.sequenceIndex] === key; + if (!isSequenceCurrent) return; + + if (ready) { + this._playSingle(key, false); + return; + } + + this.finishPlayback(key, { + advanceSequence: true, + emitEnded: true, + force: true, + }); + }) + .finally(() => { + this.pendingPlayKeys.delete(key); + }); + } + // 内部方法:播放单个 key _playSingle(key, forceStopAll = false) { if (this.isInterrupted) { @@ -726,7 +808,7 @@ class AudioManager { } const audio = this.audioMap.get(key); - if (audio) { + if (audio && this.readyMap.get(key)) { // 播放前确保遵循当前静音状态 try { if (typeof audio.volume === "number") { @@ -768,32 +850,8 @@ class AudioManager { this.lastPlayAt = Date.now(); this.startPlayWatchdog(key); } else { - debugLog(`音频 ${key} 不存在,尝试重新加载...`); - this.retryLoadAudio(key); - let loadWaitTimer = null; - const cleanup = () => { - try { - uni.$off("audioLoaded", handler); - } catch (_) {} - if (loadWaitTimer) { - clearTimeout(loadWaitTimer); - loadWaitTimer = null; - } - }; - const handler = (loadedKey) => { - if (loadedKey === key) { - cleanup(); - // 再次校验是否存在且就绪 - const a = this.audioMap.get(key); - if (a && this.readyMap.get(key)) { - this._playSingle(key, false); - } - } - }; - try { - uni.$on("audioLoaded", handler); - } catch (_) {} - loadWaitTimer = setTimeout(cleanup, 12000); + debugLog(`音频 ${key} 尚未就绪,按需加载后播放...`); + this.waitForAudioAndPlay(key); } } @@ -886,9 +944,11 @@ class AudioManager { debugLog("本地音频缓存清理完成"); } - // 手动重置并重新加载所有音频(用于卡住时恢复) + // 手动重置音频实例,并按之前的预热范围在后台恢复。 reloadAll() { - debugLog("执行 reloadAll: 重置所有状态并重新加载"); + debugLog("执行 reloadAll: 重置音频实例并后台恢复"); + const shouldWarmAll = this.warmAllPromise !== null; + this.loadGeneration += 1; // 1. 停止所有播放 this.stopAll(); @@ -903,8 +963,9 @@ class AudioManager { // 3. 重置状态 this.readyMap.clear(); - this.failedLoadKeys.clear(); this.allowPlayMap.clear(); + this.loadingKeyPromises.clear(); + this.pendingPlayKeys.clear(); this.clearAllPlayWatchdogs(); this.currentPlayingKey = null; this.sequenceQueue = []; @@ -915,13 +976,12 @@ class AudioManager { // 这里选择不自动全清,而是依赖 onError 里的单点清除。如果需要彻底重置,可取消注释: // this.clearCache(); - // 4. 强制重置加载锁 + // 4. 重置后台预热状态 this.isLoading = false; - this.loadingPromise = null; - this.currentLoadingIndex = 0; + this.warmAllPromise = null; - // 5. 重新初始化 (initAudios 会自增 loadGeneration,从而终止之前的任何异步循环) - return this.initAudios(); + // 5. 之前已启动过全量预热则继续全量恢复,否则只恢复按钮音效。 + return shouldWarmAll ? this.warmAll() : this.warmButton(); } } diff --git a/src/components/BackToGame.vue b/src/components/BackToGame.vue index c8e044d..1ebaf2c 100644 --- a/src/components/BackToGame.vue +++ b/src/components/BackToGame.vue @@ -67,7 +67,6 @@ const onClick = debounce(async () => { loading.value = true; const result = await getBattleAPI(); if (result && result.matchId) { - await uni.$checkAudio(); await returnToBattle(result, navigateOnce); return; } diff --git a/src/components/Container.vue b/src/components/Container.vue index 46106aa..93d652a 100644 --- a/src/components/Container.vue +++ b/src/components/Container.vue @@ -1,5 +1,5 @@