From efa16c64a613d3239356986a07135613131cf6cf Mon Sep 17 00:00:00 2001 From: kron Date: Thu, 27 Nov 2025 18:16:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=9F=B3=E9=A2=91=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/audioManager.js | 434 +++++++++++++++-------------------- src/components/Container.vue | 2 +- 2 files changed, 185 insertions(+), 251 deletions(-) diff --git a/src/audioManager.js b/src/audioManager.js index 03ac17e..b7e8d81 100644 --- a/src/audioManager.js +++ b/src/audioManager.js @@ -97,8 +97,10 @@ class AudioManager { constructor() { this.audioMap = new Map(); this.currentPlayingKey = null; - this.retryCount = new Map(); this.maxRetries = 3; + // 多轮统一重试:最多重试的轮次与每轮间隔 + this.maxRetryRounds = 10; + this.retryRoundIntervalMs = 1500; // 显式授权播放标记,防止 iOS 在设置 src 后误播 this.allowPlayMap = new Map(); @@ -119,196 +121,221 @@ class AudioManager { // 静音开关 this.isMuted = false; - - // 网络状态相关 - this.networkOnline = true; this.pendingPlayKey = null; // 新增:就绪状态映射 this.readyMap = new Map(); - try { - uni.onNetworkStatusChange(({ isConnected }) => { - this.networkOnline = !!isConnected; - if (this.networkOnline) { - this.onNetworkRestored(); - } else { - this.onNetworkLost(); - } - }); - } catch (_) {} - + // 新增:首轮失败的音频集合与重试阶段标识 + this.failedLoadKeys = new Set(); this.initAudios(); } - // 初始化音频 + // 初始化音频(两阶段:首轮串行加载全部,次轮仅串行加载失败项一次) initAudios() { if (this.isLoading) { debugLog("音频正在加载中,跳过重复初始化"); return this.loadingPromise; } - debugLog("开始串行加载音频..."); this.isLoading = true; this.audioKeys = Object.keys(audioFils); this.currentLoadingIndex = 0; + this.failedLoadKeys.clear(); this.loadingPromise = new Promise((resolve) => { - this.loadNextAudio(resolve); - }); + const finalize = () => { + const runRounds = (round) => { + // 达到最大轮次或没有失败项,收尾 + 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 (this.failedLoadKeys.size > 0 && round < this.maxRetryRounds) { + setTimeout(() => runRounds(round + 1), this.retryRoundIntervalMs); + } else { + this.isLoading = false; + resolve(); + } + }); + }; + + // 启动第 1 轮重试(如有失败项) + runRounds(1); + }; + this.loadNextAudio(finalize); + }); return this.loadingPromise; } - // 串行加载下一个音频 + // 按自定义列表串行加载音频(避免并发过多) + loadKeysSequentially(keys, onComplete) { + let idx = 0; + const list = Array.from(keys); + const next = () => { + if (idx >= list.length) { + if (onComplete) onComplete(); + return; + } + const k = list[idx++]; + + // 已就绪的音频不再重载,避免把 ready 状态重置为 false + if (this.readyMap.get(k)) { + setTimeout(next, 50); + return; + } + + // 未就绪:已存在则重载;不存在则创建 + if (this.audioMap.has(k)) { + this.retryLoadAudio(k); + setTimeout(next, 100); + } else { + this.createAudio(k, () => { + setTimeout(next, 100); + }); + return; // createAudio 内部会触发 next + } + }; + next(); + } + + // 串行加载下一个音频(首轮) loadNextAudio(onComplete) { if (this.currentLoadingIndex >= this.audioKeys.length) { - debugLog("所有音频加载完成"); - this.isLoading = false; + debugLog("首轮加载遍历完成", this.currentLoadingIndex); if (onComplete) onComplete(); return; } - const key = this.audioKeys[this.currentLoadingIndex]; debugLog( `开始加载音频 ${this.currentLoadingIndex + 1}/${ this.audioKeys.length }: ${key}` ); - this.createAudio(key, () => { - this.currentLoadingIndex++; setTimeout(() => { this.loadNextAudio(onComplete); }, 100); }); } - // 创建单个音频实例 + // 创建单个音频实例(统一在失败时记录,而非立即重试) createAudio(key, callback) { + this.currentLoadingIndex++; const src = audioFils[key]; - const audio = uni.createInnerAudioContext(); - audio.autoplay = false; - audio.src = src; - // 初始化音量(支持的平台用 volume,H5 也可用 muted 作为兜底) - try { - if (typeof audio.volume === "number") { - audio.volume = this.isMuted ? 0 : 1; - } else if (typeof audio.muted !== "undefined") { - audio.muted = this.isMuted; - } - } catch (_) {} + 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; + } else if (typeof audio.muted !== "undefined") { + audio.muted = this.isMuted; + } + } catch (_) {} + this.allowPlayMap.set(key, false); + audio.onPlay(() => { + if (!this.allowPlayMap.get(key)) { + try { + audio.stop(); + } catch (_) {} + } + }); - // 初始化为不允许播放,只有显式 play() 才允许 - this.allowPlayMap.set(key, false); - - // 防止 iOS 误播:非授权播放立刻停止 - audio.onPlay(() => { - if (!this.allowPlayMap.get(key)) { + const loadTimeout = setTimeout(() => { + debugLog(`音频 ${key} 加载超时`); + this.recordLoadFailure(key); try { - audio.stop(); + audio.destroy(); } catch (_) {} - } + if (callback) callback(); + }, 10000); + + audio.onCanplay(() => { + if (!this.allowPlayMap.get(key)) { + try { + audio.pause(); + } catch (_) {} + } + clearTimeout(loadTimeout); + 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(); + }); + + audio.onError((res) => { + clearTimeout(loadTimeout); + debugLog(`音频 ${key} 加载失败:`, res.errMsg); + this.recordLoadFailure(key); + this.audioMap.delete(key); + audio.destroy(); + if (this.readyMap.get(key)) { + this.readyMap.set(key, false); + } else { + if (callback) callback(); + } + }); + + audio.onEnded(() => { + if (this.currentPlayingKey === key) { + this.currentPlayingKey = null; + } + this.allowPlayMap.set(key, false); + this.onAudioEnded(key); + }); + + audio.onStop(() => { + if (this.currentPlayingKey === key) { + this.currentPlayingKey = null; + } + this.allowPlayMap.set(key, false); + }); + + this.audioMap.set(key, audio); + }; + + // 统一先下载到本地再加载 + uni.downloadFile({ + url: src, + timeout: 20000, + success: (res) => { + // 成功必须有临时文件路径 + if (res.tempFilePath) { + setupAudio(res.tempFilePath); + } else { + this.recordLoadFailure(key); + if (callback) callback(); + } + }, + fail: () => { + this.recordLoadFailure(key); + if (callback) callback(); + }, }); - - // 设置加载超时 - const loadTimeout = setTimeout(() => { - debugLog(`音频 ${key} 加载超时`); - audio.destroy(); - if (callback) callback(); - }, 10000); - - // 监听加载状态 - audio.onCanplay(() => { - // 预加载阶段:仅在未授权情况下暂停,避免用户刚点击播放被打断 - if (!this.allowPlayMap.get(key)) { - try { - audio.pause(); - } catch (_) {} - } - clearTimeout(loadTimeout); - // 标记为已就绪 - this.readyMap.set(key, true); - debugLog(`音频 ${key} 已加载完成`); - uni.$emit("audioLoaded", key); - const loadedAudioKeys = uni.getStorageSync("loadedAudioKeys") || {}; - loadedAudioKeys[key] = true; - uni.setStorageSync("loadedAudioKeys", loadedAudioKeys); - this.retryCount.set(key, 0); - if (callback) callback(); - }); - - audio.onError((res) => { - clearTimeout(loadTimeout); - debugLog(`音频 ${key} 加载失败:`, res.errMsg); - this.allowPlayMap.set(key, false); - // 标记为未就绪 - this.readyMap.set(key, false); - this.handleAudioError(key); - if (callback) callback(); - }); - - // 监听播放结束事件 - audio.onEnded(() => { - if (this.currentPlayingKey === key) { - this.currentPlayingKey = null; - } - this.allowPlayMap.set(key, false); - // 触发连续播放队列的衔接 - this.onAudioEnded(key); - }); - - // 监听播放停止事件 - audio.onStop(() => { - if (this.currentPlayingKey === key) { - this.currentPlayingKey = null; - } - this.allowPlayMap.set(key, false); - }); - - this.audioMap.set(key, audio); - if (!this.retryCount.has(key)) { - this.retryCount.set(key, 0); - } } - // 处理音频加载错误 - handleAudioError(key) { - // 网络不可用时不重试,等重连后统一重建 - if (!this.networkOnline) { - debugLog(`网络不可用,暂不重试音频: ${key}`); - return; - } - const currentRetries = this.retryCount.get(key) || 0; - - if (currentRetries < this.maxRetries) { - this.retryCount.set(key, currentRetries + 1); - debugLog(`音频 ${key} 开始第 ${currentRetries + 1} 次重试...`); - - setTimeout(() => { - this.retryLoadAudio(key); - }, 1000); - } else { - console.error( - `音频 ${key} 重试 ${this.maxRetries} 次后仍然失败,停止重试` - ); - const failedAudio = this.audioMap.get(key); - if (failedAudio) { - failedAudio.destroy(); - this.audioMap.delete(key); - } - } + // 新增:记录失败(首轮与次轮都会用到) + recordLoadFailure(key) { + this.failedLoadKeys.add(key); } // 重新加载音频 retryLoadAudio(key) { - if (!this.networkOnline) { - debugLog(`网络不可用,稍后再重载音频: ${key}`); - return; - } const oldAudio = this.audioMap.get(key); - if (oldAudio) { - oldAudio.destroy(); - } + if (oldAudio) oldAudio.destroy(); this.createAudio(key); } @@ -374,17 +401,6 @@ class AudioManager { return; } - if (!this.networkOnline) { - const audio = this.audioMap.get(key); - const isReady = this.readyMap && this.readyMap.get(key); - // 离线但已就绪:允许直接播放;未就绪则记录等待重连 - if (!audio || !isReady) { - this.pendingPlayKey = key; - debugLog(`网络不可用,记录播放: ${key}`); - return; - } - } - if (forceStopAll) { this.stopAll(); } else if (this.currentPlayingKey && this.currentPlayingKey !== key) { @@ -427,7 +443,22 @@ class AudioManager { this.lastPlayAt = Date.now(); } else { debugLog(`音频 ${key} 不存在,尝试重新加载...`); - this.reloadAudio(key); + this.retryLoadAudio(key); + const handler = (loadedKey) => { + if (loadedKey === key) { + try { + uni.$off("audioLoaded", handler); + } catch (_) {} + // 再次校验是否存在且就绪 + const a = this.audioMap.get(key); + if (a && this.readyMap.get(key)) { + this._playSingle(key, false); + } + } + }; + try { + uni.$on("audioLoaded", handler); + } catch (_) {} } } @@ -473,99 +504,6 @@ class AudioManager { this.currentPlayingKey = null; } - // 销毁所有音频实例并清理状态 - destroyAll() { - for (const [k, audio] of this.audioMap.entries()) { - try { - audio.destroy(); - } catch (_) {} - this.allowPlayMap.delete(k); - this.retryCount.delete(k); - this.audioMap.delete(k); - this.readyMap.delete(k); - } - this.audioKeys = []; - this.currentLoadingIndex = 0; - this.isLoading = false; - this.loadingPromise = null; - this.currentPlayingKey = null; - } - - // 断网回调:停止当前播放,避免误状态 - onNetworkLost() { - try { - this.stopAll(); - } catch (_) {} - } - - // 重连回调:重建全部音频后,重放上一次的播放 - onNetworkRestored() { - // 网络恢复后,仅继续加载未就绪或缺失的音频,已就绪的不动,保证离线可播的缓存不被重置 - const keys = Object.keys(audioFils); - const needLoad = keys.filter( - (k) => !this.readyMap.get(k) || !this.audioMap.has(k) - ); - - if (needLoad.length > 0) { - this.loadKeysSequentially(needLoad, () => { - if (this.pendingPlayKey) { - const pending = this.pendingPlayKey; - this.pendingPlayKey = null; - setTimeout(() => { - this.play(pending); - }, 20); - } - }); - } else { - // 没有需要加载的音频,直接处理待播放 - if (this.pendingPlayKey) { - const pending = this.pendingPlayKey; - this.pendingPlayKey = null; - setTimeout(() => { - this.play(pending); - }, 20); - } - } - } - - // 重连后统一重建音频实例 - reinitializeOnReconnect() { - this.destroyAll(); - this.initAudios(); - } - - // 按自定义列表串行加载音频(避免并发过多) - loadKeysSequentially(keys, onComplete) { - let idx = 0; - const next = () => { - if (idx >= keys.length) { - if (onComplete) onComplete(); - return; - } - const k = keys[idx++]; - // 已存在但未就绪:重载;不存在:创建 - if (this.audioMap.has(k)) { - this.retryLoadAudio(k); - } else { - this.createAudio(k, () => { - setTimeout(next, 100); - }); - return; // createAudio 内部会触发 next - } - setTimeout(next, 100); - }; - next(); - } - - // 手动重新加载指定音频 - reloadAudio(key) { - if (audioFils[key]) { - debugLog(`手动重新加载音频: ${key}`); - this.retryCount.set(key, 0); - this.retryLoadAudio(key); - } - } - // 设置静音开关:true 静音,false 取消静音 setMuted(muted) { this.isMuted = !!muted; @@ -583,20 +521,16 @@ class AudioManager { // 新增:返回音频加载进度(0~1) getLoadProgress() { - // 总数优先使用已初始化的 audioKeys,未初始化则回退到 audioFils - const keys = - this.audioKeys && this.audioKeys.length > 0 - ? this.audioKeys - : Object.keys(audioFils); + const keys = Object.keys(audioFils); const total = keys.length; if (total === 0) return 0; let loaded = 0; for (const k of keys) { if (this.readyMap.get(k)) loaded++; } - return loaded / total; + return Number((loaded / total).toFixed(2)); } } // 导出单例 -export default new AudioManager(); \ No newline at end of file +export default new AudioManager(); diff --git a/src/components/Container.vue b/src/components/Container.vue index d10098e..698eb1a 100644 --- a/src/components/Container.vue +++ b/src/components/Container.vue @@ -85,7 +85,7 @@ const checkAudioProgress = async () => { const audioFinalProgress = computed(() => { const left = 1 - audioInitProgress.value; - return (audioProgress.value - audioInitProgress.value) / left; + return Math.max(0, (audioProgress.value - audioInitProgress.value) / left); }); onMounted(() => {