修改音频加载策略

This commit is contained in:
kron
2025-11-27 18:16:32 +08:00
parent 82a0ee83b2
commit efa16c64a6
2 changed files with 185 additions and 251 deletions

View File

@@ -97,8 +97,10 @@ class AudioManager {
constructor() { constructor() {
this.audioMap = new Map(); this.audioMap = new Map();
this.currentPlayingKey = null; this.currentPlayingKey = null;
this.retryCount = new Map();
this.maxRetries = 3; this.maxRetries = 3;
// 多轮统一重试:最多重试的轮次与每轮间隔
this.maxRetryRounds = 10;
this.retryRoundIntervalMs = 1500;
// 显式授权播放标记,防止 iOS 在设置 src 后误播 // 显式授权播放标记,防止 iOS 在设置 src 后误播
this.allowPlayMap = new Map(); this.allowPlayMap = new Map();
@@ -119,77 +121,119 @@ class AudioManager {
// 静音开关 // 静音开关
this.isMuted = false; this.isMuted = false;
// 网络状态相关
this.networkOnline = true;
this.pendingPlayKey = null; this.pendingPlayKey = null;
// 新增:就绪状态映射 // 新增:就绪状态映射
this.readyMap = new Map(); this.readyMap = new Map();
try { // 新增:首轮失败的音频集合与重试阶段标识
uni.onNetworkStatusChange(({ isConnected }) => { this.failedLoadKeys = new Set();
this.networkOnline = !!isConnected;
if (this.networkOnline) {
this.onNetworkRestored();
} else {
this.onNetworkLost();
}
});
} catch (_) {}
this.initAudios(); this.initAudios();
} }
// 初始化音频 // 初始化音频(两阶段:首轮串行加载全部,次轮仅串行加载失败项一次)
initAudios() { initAudios() {
if (this.isLoading) { if (this.isLoading) {
debugLog("音频正在加载中,跳过重复初始化"); debugLog("音频正在加载中,跳过重复初始化");
return this.loadingPromise; return this.loadingPromise;
} }
debugLog("开始串行加载音频..."); debugLog("开始串行加载音频...");
this.isLoading = true; this.isLoading = true;
this.audioKeys = Object.keys(audioFils); this.audioKeys = Object.keys(audioFils);
this.currentLoadingIndex = 0; this.currentLoadingIndex = 0;
this.failedLoadKeys.clear();
this.loadingPromise = new Promise((resolve) => { this.loadingPromise = new Promise((resolve) => {
this.loadNextAudio(resolve); const finalize = () => {
}); const runRounds = (round) => {
// 达到最大轮次或没有失败项,收尾
return this.loadingPromise; if (this.failedLoadKeys.size === 0 || round > this.maxRetryRounds) {
}
// 串行加载下一个音频
loadNextAudio(onComplete) {
if (this.currentLoadingIndex >= this.audioKeys.length) {
debugLog("所有音频加载完成");
this.isLoading = false; this.isLoading = false;
if (onComplete) onComplete(); resolve();
return; 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.currentLoadingIndex);
if (onComplete) onComplete();
return;
}
const key = this.audioKeys[this.currentLoadingIndex]; const key = this.audioKeys[this.currentLoadingIndex];
debugLog( debugLog(
`开始加载音频 ${this.currentLoadingIndex + 1}/${ `开始加载音频 ${this.currentLoadingIndex + 1}/${
this.audioKeys.length this.audioKeys.length
}: ${key}` }: ${key}`
); );
this.createAudio(key, () => { this.createAudio(key, () => {
this.currentLoadingIndex++;
setTimeout(() => { setTimeout(() => {
this.loadNextAudio(onComplete); this.loadNextAudio(onComplete);
}, 100); }, 100);
}); });
} }
// 创建单个音频实例 // 创建单个音频实例(统一在失败时记录,而非立即重试)
createAudio(key, callback) { createAudio(key, callback) {
this.currentLoadingIndex++;
const src = audioFils[key]; const src = audioFils[key];
const setupAudio = (realSrc) => {
const audio = uni.createInnerAudioContext(); const audio = uni.createInnerAudioContext();
audio.autoplay = false; audio.autoplay = false;
audio.src = src; audio.src = realSrc;
// 初始化音量(支持的平台用 volumeH5 也可用 muted 作为兜底)
try { try {
if (typeof audio.volume === "number") { if (typeof audio.volume === "number") {
audio.volume = this.isMuted ? 0 : 1; audio.volume = this.isMuted ? 0 : 1;
@@ -197,11 +241,7 @@ class AudioManager {
audio.muted = this.isMuted; audio.muted = this.isMuted;
} }
} catch (_) {} } catch (_) {}
// 初始化为不允许播放,只有显式 play() 才允许
this.allowPlayMap.set(key, false); this.allowPlayMap.set(key, false);
// 防止 iOS 误播:非授权播放立刻停止
audio.onPlay(() => { audio.onPlay(() => {
if (!this.allowPlayMap.get(key)) { if (!this.allowPlayMap.get(key)) {
try { try {
@@ -210,54 +250,53 @@ class AudioManager {
} }
}); });
// 设置加载超时
const loadTimeout = setTimeout(() => { const loadTimeout = setTimeout(() => {
debugLog(`音频 ${key} 加载超时`); debugLog(`音频 ${key} 加载超时`);
this.recordLoadFailure(key);
try {
audio.destroy(); audio.destroy();
} catch (_) {}
if (callback) callback(); if (callback) callback();
}, 10000); }, 10000);
// 监听加载状态
audio.onCanplay(() => { audio.onCanplay(() => {
// 预加载阶段:仅在未授权情况下暂停,避免用户刚点击播放被打断
if (!this.allowPlayMap.get(key)) { if (!this.allowPlayMap.get(key)) {
try { try {
audio.pause(); audio.pause();
} catch (_) {} } catch (_) {}
} }
clearTimeout(loadTimeout); clearTimeout(loadTimeout);
// 标记为已就绪
this.readyMap.set(key, true); this.readyMap.set(key, true);
debugLog(`音频 ${key} 已加载完成`); this.failedLoadKeys.delete(key);
debugLog(`音频 ${key} 已加载完成`, this.getLoadProgress());
uni.$emit("audioLoaded", key); uni.$emit("audioLoaded", key);
const loadedAudioKeys = uni.getStorageSync("loadedAudioKeys") || {}; const loadedAudioKeys = uni.getStorageSync("loadedAudioKeys") || {};
loadedAudioKeys[key] = true; loadedAudioKeys[key] = true;
uni.setStorageSync("loadedAudioKeys", loadedAudioKeys); uni.setStorageSync("loadedAudioKeys", loadedAudioKeys);
this.retryCount.set(key, 0);
if (callback) callback(); if (callback) callback();
}); });
audio.onError((res) => { audio.onError((res) => {
clearTimeout(loadTimeout); clearTimeout(loadTimeout);
debugLog(`音频 ${key} 加载失败:`, res.errMsg); debugLog(`音频 ${key} 加载失败:`, res.errMsg);
this.allowPlayMap.set(key, false); this.recordLoadFailure(key);
// 标记为未就绪 this.audioMap.delete(key);
audio.destroy();
if (this.readyMap.get(key)) {
this.readyMap.set(key, false); this.readyMap.set(key, false);
this.handleAudioError(key); } else {
if (callback) callback(); if (callback) callback();
}
}); });
// 监听播放结束事件
audio.onEnded(() => { audio.onEnded(() => {
if (this.currentPlayingKey === key) { if (this.currentPlayingKey === key) {
this.currentPlayingKey = null; this.currentPlayingKey = null;
} }
this.allowPlayMap.set(key, false); this.allowPlayMap.set(key, false);
// 触发连续播放队列的衔接
this.onAudioEnded(key); this.onAudioEnded(key);
}); });
// 监听播放停止事件
audio.onStop(() => { audio.onStop(() => {
if (this.currentPlayingKey === key) { if (this.currentPlayingKey === key) {
this.currentPlayingKey = null; this.currentPlayingKey = null;
@@ -266,49 +305,37 @@ class AudioManager {
}); });
this.audioMap.set(key, audio); this.audioMap.set(key, audio);
if (!this.retryCount.has(key)) { };
this.retryCount.set(key, 0);
}
}
// 处理音频加载错误 // 统一先下载到本地再加载
handleAudioError(key) { uni.downloadFile({
// 网络不可用时不重试,等重连后统一重建 url: src,
if (!this.networkOnline) { timeout: 20000,
debugLog(`网络不可用,暂不重试音频: ${key}`); success: (res) => {
return; // 成功必须有临时文件路径
} if (res.tempFilePath) {
const currentRetries = this.retryCount.get(key) || 0; setupAudio(res.tempFilePath);
if (currentRetries < this.maxRetries) {
this.retryCount.set(key, currentRetries + 1);
debugLog(`音频 ${key} 开始第 ${currentRetries + 1} 次重试...`);
setTimeout(() => {
this.retryLoadAudio(key);
}, 1000);
} else { } else {
console.error( this.recordLoadFailure(key);
`音频 ${key} 重试 ${this.maxRetries} 次后仍然失败,停止重试` if (callback) callback();
);
const failedAudio = this.audioMap.get(key);
if (failedAudio) {
failedAudio.destroy();
this.audioMap.delete(key);
} }
},
fail: () => {
this.recordLoadFailure(key);
if (callback) callback();
},
});
} }
// 新增:记录失败(首轮与次轮都会用到)
recordLoadFailure(key) {
this.failedLoadKeys.add(key);
} }
// 重新加载音频 // 重新加载音频
retryLoadAudio(key) { retryLoadAudio(key) {
if (!this.networkOnline) {
debugLog(`网络不可用,稍后再重载音频: ${key}`);
return;
}
const oldAudio = this.audioMap.get(key); const oldAudio = this.audioMap.get(key);
if (oldAudio) { if (oldAudio) oldAudio.destroy();
oldAudio.destroy();
}
this.createAudio(key); this.createAudio(key);
} }
@@ -374,17 +401,6 @@ class AudioManager {
return; 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) { if (forceStopAll) {
this.stopAll(); this.stopAll();
} else if (this.currentPlayingKey && this.currentPlayingKey !== key) { } else if (this.currentPlayingKey && this.currentPlayingKey !== key) {
@@ -427,7 +443,22 @@ class AudioManager {
this.lastPlayAt = Date.now(); this.lastPlayAt = Date.now();
} else { } else {
debugLog(`音频 ${key} 不存在,尝试重新加载...`); 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; 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 取消静音 // 设置静音开关true 静音false 取消静音
setMuted(muted) { setMuted(muted) {
this.isMuted = !!muted; this.isMuted = !!muted;
@@ -583,18 +521,14 @@ class AudioManager {
// 新增返回音频加载进度0~1 // 新增返回音频加载进度0~1
getLoadProgress() { getLoadProgress() {
// 总数优先使用已初始化的 audioKeys未初始化则回退到 audioFils const keys = Object.keys(audioFils);
const keys =
this.audioKeys && this.audioKeys.length > 0
? this.audioKeys
: Object.keys(audioFils);
const total = keys.length; const total = keys.length;
if (total === 0) return 0; if (total === 0) return 0;
let loaded = 0; let loaded = 0;
for (const k of keys) { for (const k of keys) {
if (this.readyMap.get(k)) loaded++; if (this.readyMap.get(k)) loaded++;
} }
return loaded / total; return Number((loaded / total).toFixed(2));
} }
} }

View File

@@ -85,7 +85,7 @@ const checkAudioProgress = async () => {
const audioFinalProgress = computed(() => { const audioFinalProgress = computed(() => {
const left = 1 - audioInitProgress.value; const left = 1 - audioInitProgress.value;
return (audioProgress.value - audioInitProgress.value) / left; return Math.max(0, (audioProgress.value - audioInitProgress.value) / left);
}); });
onMounted(() => { onMounted(() => {