修改音频加载策略
This commit is contained in:
@@ -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,18 +521,14 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user