Files
shoot-miniprograms/src/audioManager.js
T

928 lines
29 KiB
JavaScript

export const AUDIO_INTERRUPTION_BEGIN_EVENT = "audio-interruption-begin";
export const AUDIO_INTERRUPTION_END_EVENT = "audio-interruption-end";
export const audioFils = {
tententen: "https://static.shelingxingqiu.com/shootmini/static/audio/tententen.mp3",
点击按钮: "https://static.shelingxingqiu.com/shootmini/static/audio/%E7%82%B9%E5%87%BB%E6%8C%89%E9%92%AE.mp3",
"20CM全环靶": "https://static.shelingxingqiu.com/shootmini/static/audio/20CM%E5%85%A8%E7%8E%AF%E9%9D%B6-%E6%97%A0%E6%95%88.mp3",
"40CM全环靶": "https://static.shelingxingqiu.com/shootmini/static/audio/40CM%E5%85%A8%E7%8E%AF%E9%9D%B6-%E6%97%A0%E6%95%88.mp3",
// 激光已校准:
// "https://static.shelingxingqiu.com/attachment/2025-10-29/ddupaur1vdkyhzaqdc.mp3",
胜利: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcuo9yjp0kt5msvmvd.mp3",
失败: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcuo9yht2sdwhuqygy.mp3",
请射箭测试距离:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcuvj8avzqyw4hpq7t.mp3",
距离合格:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutwrda0amn5kqr4j.mp3",
距离不足:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6hr2faw28t0ianh0.mp3",
轮到你了:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutzdrn4lxcpv8aqr.mp3",
第一轮:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutyyr9a7m1vz2w13.mp3",
第二轮:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutyyr9ldnfexjxtw.mp3",
第三轮:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutyyr97m4ipxaze4.mp3",
第四轮:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutyyr9x5addohlzf.mp3",
第五轮:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutyyr9d7lw2gebpv.mp3",
决金箭轮:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutwrd9zs4oi2kujv.mp3",
请蓝方射箭:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutzdrxcbe5ll46as.mp3",
请红方射箭:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutzdrl3re3dhlfjd.mp3",
中场休息:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutwrd9zdk1xyolst.mp3",
比赛结束:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutya59b6pu0ur4um.mp3",
比赛开始:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcuu5z3a3lumkutske.mp3",
请开始射击:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutzdrl5u0iromqhf.mp3",
射击无效:
"https://static.shelingxingqiu.com/shootmini/static/audio/%E5%B0%84%E7%AE%AD%E6%97%A0%E6%95%88%E6%A3%80%E6%9F%A5%E8%B7%9D%E7%A6%BB%E5%92%8C%E9%9D%B6%E7%BA%B8.mp3",
未上靶:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6n45o3tsm1v4unam.mp3",
"1环":
"https://static.shelingxingqiu.com/shootaudio/v3/1.mp3",
"2环":
"https://static.shelingxingqiu.com/shootaudio/v3/2.mp3",
"3环":
"https://static.shelingxingqiu.com/shootaudio/v3/3.mp3",
"4环":
"https://static.shelingxingqiu.com/shootaudio/v3/4.mp3",
"5环":
"https://static.shelingxingqiu.com/shootaudio/v3/5.mp3",
"6环":
"https://static.shelingxingqiu.com/shootaudio/v3/6.mp3",
"7环":
"https://static.shelingxingqiu.com/shootaudio/v3/7.mp3",
"8环":
"https://static.shelingxingqiu.com/shootaudio/v3/8.mp3",
"9环":
"https://static.shelingxingqiu.com/shootaudio/v3/9.mp3",
"10环":
"https://static.shelingxingqiu.com/shootaudio/v3/10.mp3",
X环: "https://static.shelingxingqiu.com/shootaudio/v4/v4/X%E7%8E%AF.mp3",
向上调整:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6ellf5pfvu3l8dhr.mp3",
向右上调整:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6ellf45v88pirarr.mp3",
向右调整:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6elleqnhrenggxsb.mp3",
向右下调整:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6elleo6q16qctf6a.mp3",
向下调整:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6ellek2mu2cri2n9.mp3",
向左下调整:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6ellf25yu1pt2k5r.mp3",
向左调整:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6ellen3zoalxcb06.mp3",
向左上调整:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6ellf37a2iw6w4pu.mp3",
最后30秒:
"https://static.shelingxingqiu.com/attachment/2025-11-13/de7kzzllq0futwynso.mp3",
练习开始:
"https://static.shelingxingqiu.com/attachment/2025-11-14/de88w0lmmt43nnfmoi.mp3",
射箭声音:
"https://static.shelingxingqiu.com/shootaudio/v4/v4/%E7%AE%AD%E9%A3%9E%E8%A1%8C.mp3",
命中:
"https://static.shelingxingqiu.com/shootaudio/%E5%91%BD%E4%B8%AD.mp3"
};
// 版本控制日志函数
function debugLog(...args) {
// 获取当前环境信息
const accountInfo = uni.getAccountInfoSync();
const envVersion = accountInfo.miniProgram.envVersion;
// 只在体验版打印日志,正式版(release)和开发版(develop)不打印
if (envVersion === "trial" || envVersion === "develop") {
console.log(...args);
}
}
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.sequenceQueue = [];
this.sequenceIndex = 0;
this.isSequenceRunning = false;
// 防重复播放保护
this.lastPlayKey = null;
this.lastPlayAt = 0;
this.isInterrupted = false;
this.interruptedAt = 0;
this.interruptionFallbackMs = 5000;
this.playWatchdogMs = 8000;
this.playWatchdogTimers = new Map();
// 静音开关
this.isMuted = false;
this.pendingPlayKey = null;
// 新增:就绪状态映射
this.readyMap = new Map();
// 新增:首轮失败的音频集合与重试阶段标识
this.failedLoadKeys = new Set();
// 加载代数,用于 reloadAll 时作废旧的加载循环
this.loadGeneration = 0;
// 本地路径缓存 Map: { url: localPath }
this.localFileCache = uni.getStorageSync("audio_local_files") || {};
// 启动时自动清理过期的缓存文件(URL 已不在 audioFils 中的文件)
this.cleanObsoleteCache();
this.bindAudioInterruptionEvents();
this.initAudios();
}
bindAudioInterruptionEvents() {
if (this._audioInterruptionBound) return;
this._audioInterruptionBound = true;
const begin = () => {
if (this.isInterrupted) return;
this.isInterrupted = true;
this.interruptedAt = Date.now();
this.stopAll();
this.isSequenceRunning = false;
this.sequenceQueue = [];
this.sequenceIndex = 0;
this.pendingPlayKey = null;
uni.$emit(AUDIO_INTERRUPTION_BEGIN_EVENT);
};
const end = () => {
if (!this.isInterrupted) return;
this.isInterrupted = false;
this.interruptedAt = 0;
uni.$emit(AUDIO_INTERRUPTION_END_EVENT);
void this.reloadAll();
};
if (typeof uni?.onAudioInterruptionBegin === "function") {
uni.onAudioInterruptionBegin(begin);
}
if (typeof uni?.onAudioInterruptionEnd === "function") {
uni.onAudioInterruptionEnd(end);
}
}
// 清理不再使用的缓存文件
cleanObsoleteCache() {
const activeUrls = new Set(Object.values(audioFils));
const cachedUrls = Object.keys(this.localFileCache);
let hasChanges = false;
for (const url of cachedUrls) {
if (!activeUrls.has(url)) {
debugLog(`发现废弃音频缓存,正在清理: ${url}`);
const path = this.localFileCache[url];
// 移除物理文件
uni.removeSavedFile({
filePath: path,
complete: () => {
// 忽略移除结果,直接移除记录
},
});
// 移除记录
delete this.localFileCache[url];
hasChanges = true;
}
}
if (hasChanges) {
uni.setStorageSync("audio_local_files", this.localFileCache);
debugLog("废弃缓存清理完成");
}
}
// 初始化音频(两阶段:首轮串行加载全部,次轮仅串行加载失败项一次)
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;
}
// 按自定义列表串行加载音频(避免并发过多)
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++];
// 已就绪的音频不再重载,避免把 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, gen) {
if (gen !== undefined && gen !== this.loadGeneration) return;
if (this.currentLoadingIndex >= this.audioKeys.length) {
debugLog("首轮加载遍历完成", this.currentLoadingIndex);
if (onComplete) onComplete();
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 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 (_) {}
}
});
const loadTimeout = setTimeout(() => {
debugLog(`音频 ${key} 加载超时`);
this.recordLoadFailure(key);
this.audioMap.delete(key);
try {
audio.destroy();
} catch (_) {}
this.finishPlayback(key, {
advanceSequence: true,
emitEnded: true,
});
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);
// 如果是本地文件加载失败,可能是文件损坏,清除缓存以便下次重新下载
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();
}
});
audio.onEnded(() => {
this.finishPlayback(key, {
advanceSequence: true,
emitEnded: true,
});
});
audio.onStop(() => {
this.finishPlayback(key);
});
this.audioMap.set(key, audio);
};
// 检查是否有可用的本地缓存
this.checkLocalFile(src).then((localPath) => {
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();
},
});
}
});
}
// 检查本地文件是否有效
checkLocalFile(url) {
return new Promise((resolve) => {
const path = this.localFileCache[url];
if (!path) {
resolve(null);
return;
}
// 检查文件是否存在
uni.getFileSystemManager().getFileInfo({
filePath: path,
success: () => {
resolve(path);
},
fail: () => {
// 文件不存在,清理记录
delete this.localFileCache[url];
uni.setStorageSync("audio_local_files", this.localFileCache);
resolve(null);
},
});
});
}
// 新增:记录失败(首轮与次轮都会用到)
recordLoadFailure(key) {
this.failedLoadKeys.add(key);
}
clearPlayWatchdog(key) {
const timer = this.playWatchdogTimers.get(key);
if (timer) {
clearTimeout(timer);
this.playWatchdogTimers.delete(key);
}
}
clearAllPlayWatchdogs() {
for (const timer of this.playWatchdogTimers.values()) {
clearTimeout(timer);
}
this.playWatchdogTimers.clear();
}
startPlayWatchdog(key) {
this.clearPlayWatchdog(key);
const timer = setTimeout(() => {
if (this.currentPlayingKey !== key) return;
debugLog(`音频 ${key} 播放超时,跳过当前音频并继续队列`);
this.finishPlayback(key, {
advanceSequence: true,
emitEnded: true,
force: true,
});
this.reloadAudioKey(key);
}, this.playWatchdogMs);
this.playWatchdogTimers.set(key, timer);
}
finishPlayback(key, { advanceSequence = false, emitEnded = false, force = false } = {}) {
const wasCurrent = this.currentPlayingKey === key;
const isSequenceCurrent =
this.isSequenceRunning && this.sequenceQueue[this.sequenceIndex] === key;
this.clearPlayWatchdog(key);
this.allowPlayMap.set(key, false);
if (!force && !wasCurrent && !isSequenceCurrent) return false;
if (wasCurrent) {
this.currentPlayingKey = null;
}
if (advanceSequence && isSequenceCurrent) {
this.onAudioEnded(key);
}
if (emitEnded) {
uni.$emit("audioEnded", key);
}
return true;
}
recoverFromInterruptionIfStale(force = false) {
if (!this.isInterrupted) return false;
const interruptedFor = Date.now() - (this.interruptedAt || Date.now());
if (!force && interruptedFor < this.interruptionFallbackMs) return false;
debugLog("音频中断状态超时,执行兜底恢复");
this.isInterrupted = false;
this.interruptedAt = 0;
uni.$emit(AUDIO_INTERRUPTION_END_EVENT);
void this.reloadAll();
return true;
}
recoverIfStale(expectedKey) {
if (this.recoverFromInterruptionIfStale(true)) return;
const key =
expectedKey || this.currentPlayingKey || this.sequenceQueue[this.sequenceIndex];
if (!key) {
if (this.isSequenceRunning) {
this.sequenceQueue = [];
this.sequenceIndex = 0;
this.isSequenceRunning = false;
}
return;
}
const isStaleCurrent =
this.currentPlayingKey === key ||
(this.isSequenceRunning && this.sequenceQueue[this.sequenceIndex] === key);
if (!isStaleCurrent) return;
debugLog(`音频 ${key} 等待超时,执行轻量恢复`);
const audio = this.audioMap.get(key);
if (audio) {
try {
audio.stop();
} catch (_) {}
}
this.finishPlayback(key, {
advanceSequence: true,
emitEnded: true,
force: true,
});
this.reloadAudioKey(key);
}
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);
}
// 重新加载音频
retryLoadAudio(key) {
this.clearPlayWatchdog(key);
const oldAudio = this.audioMap.get(key);
if (oldAudio) {
try {
oldAudio.destroy();
} catch (_) {}
}
this.createAudio(key);
}
// 播放指定音频或音频数组(数组则按顺序连续播放)
play(input, interrupt = true) {
if (this.isInterrupted) {
this.recoverFromInterruptionIfStale();
}
if (this.isInterrupted) {
debugLog("音频处理中断状态,忽略播放请求");
return;
}
// 统一规范化为队列
let queue = [];
if (Array.isArray(input)) {
queue = input.filter((k) => !!audioFils[k]);
} else if (typeof input === "string") {
queue = !!audioFils[input] ? [input] : [];
} else {
debugLog("play 参数类型无效,仅支持字符串或字符串数组");
return;
}
if (queue.length === 0) {
debugLog("连续播放队列为空或无效");
return;
}
if (interrupt) {
// 立即打断并启动新的播放序列
this.stopAll();
this.isSequenceRunning = false;
this.sequenceQueue = [];
this.sequenceIndex = 0;
this.sequenceQueue = queue;
this.sequenceIndex = 0;
this.isSequenceRunning = true;
this._playSingle(queue[0], false);
return;
}
// 不打断当前播放:把新的队列加入到序列中,等待当前播放结束后衔接
if (this.currentPlayingKey) {
if (this.isSequenceRunning) {
// 已有序列在跑:直接追加
this.sequenceQueue = this.sequenceQueue.concat(queue);
} else {
// 没有序列但当前有正在播放的:以当前为序列的起点
this.isSequenceRunning = true;
this.sequenceQueue = [this.currentPlayingKey].concat(queue);
this.sequenceIndex = 0;
// 不触发 _playSingle,等待当前音频自然结束后由 onAudioEnded 接管
}
} else {
// 当前没有播放:直接启动新的序列
this.sequenceQueue = queue;
this.sequenceIndex = 0;
this.isSequenceRunning = true;
this._playSingle(queue[0], false);
}
}
// 内部方法:播放单个 key
_playSingle(key, forceStopAll = false) {
if (this.isInterrupted) {
this.recoverFromInterruptionIfStale();
}
if (this.isInterrupted) {
debugLog(`音频处理中断状态,跳过播放: ${key}`);
return;
}
// 200ms 内的同 key 重复播放直接忽略,避免“比比赛开始”这类重复首音
const now = Date.now();
if (this.lastPlayKey === key && now - this.lastPlayAt < 250) {
debugLog(`忽略快速重复播放: ${key}`);
this.finishPlayback(key, {
advanceSequence: true,
emitEnded: true,
force: true,
});
return;
}
if (forceStopAll) {
this.stopAll();
} else if (this.currentPlayingKey && this.currentPlayingKey !== key) {
this.stop(this.currentPlayingKey);
} else if (this.currentPlayingKey === key) {
// 同一音频正在播放:不重启,避免听到重复开头
return;
}
const audio = this.audioMap.get(key);
if (audio) {
// 播放前确保遵循当前静音状态
try {
if (typeof audio.volume === "number") {
audio.volume = this.isMuted ? 0 : 1;
} else if (typeof audio.muted !== "undefined") {
audio.muted = this.isMuted;
}
} catch (_) {}
// 同一音频:避免 stop() 触发 onStop 清除授权,使用 pause()+seek(0)
try {
audio.pause();
} catch (_) {}
try {
if (typeof audio.seek === "function") {
audio.seek(0);
} else {
audio.startTime = 0;
}
} catch (_) {
audio.startTime = 0;
}
// 显式授权播放并立即播放
this.allowPlayMap.set(key, true);
try {
audio.play();
} catch (err) {
this.finishPlayback(key, {
advanceSequence: true,
emitEnded: true,
force: true,
});
debugLog(`音频 ${key} 播放调用失败`, err?.errMsg || err);
return;
}
this.currentPlayingKey = key;
this.lastPlayKey = key;
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);
}
}
// 连续播放:在某个音频结束后,若处于队列播放状态则继续下一个
onAudioEnded(key) {
if (!this.isSequenceRunning) return;
const currentKey = this.sequenceQueue[this.sequenceIndex];
if (currentKey !== key) return;
const nextIndex = this.sequenceIndex + 1;
if (nextIndex < this.sequenceQueue.length) {
this.sequenceIndex = nextIndex;
const nextKey = this.sequenceQueue[nextIndex];
this._playSingle(nextKey, false);
} else {
// 队列播放完成
this.isSequenceRunning = false;
this.sequenceQueue = [];
this.sequenceIndex = 0;
}
}
// 停止指定音频
stop(key) {
const audio = this.audioMap.get(key);
this.clearPlayWatchdog(key);
if (audio) {
audio.stop();
this.allowPlayMap.set(key, false);
if (this.currentPlayingKey === key) {
this.currentPlayingKey = null;
}
}
}
// 停止所有音频
stopAll() {
this.clearAllPlayWatchdogs();
for (const [k, audio] of this.audioMap.entries()) {
try {
audio.stop();
} catch (_) {}
this.allowPlayMap.set(k, false);
}
this.currentPlayingKey = null;
}
// 设置静音开关:true 静音,false 取消静音
setMuted(muted) {
this.isMuted = !!muted;
for (const audio of this.audioMap.values()) {
try {
if (typeof audio.volume === "number") {
audio.volume = this.isMuted ? 0 : 1;
} else if (typeof audio.muted !== "undefined") {
audio.muted = this.isMuted;
}
} catch (_) {}
}
debugLog(`静音状态已设置为: ${this.isMuted}`);
}
// 新增:返回音频加载进度(0~1)
getLoadProgress() {
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 Number((loaded / total).toFixed(2));
}
// 清理本地音频缓存文件
clearCache() {
debugLog("开始清理本地音频缓存...");
const cache = uni.getStorageSync("audio_local_files") || {};
const paths = Object.values(cache);
for (const path of paths) {
uni.removeSavedFile({
filePath: path,
complete: (res) => {
// 无论成功失败都继续
},
});
}
uni.removeStorageSync("audio_local_files");
this.localFileCache = {};
debugLog("本地音频缓存清理完成");
}
// 手动重置并重新加载所有音频(用于卡住时恢复)
reloadAll() {
debugLog("执行 reloadAll: 重置所有状态并重新加载");
// 1. 停止所有播放
this.stopAll();
// 2. 销毁现有音频实例
for (const audio of this.audioMap.values()) {
try {
audio.destroy();
} catch (_) {}
}
this.audioMap.clear();
// 3. 重置状态
this.readyMap.clear();
this.failedLoadKeys.clear();
this.allowPlayMap.clear();
this.clearAllPlayWatchdogs();
this.currentPlayingKey = null;
this.sequenceQueue = [];
this.sequenceIndex = 0;
this.isSequenceRunning = false;
// 清理一下可能损坏的缓存(可选,如果用户因为缓存坏了卡住,这一步很有用)
// 这里选择不自动全清,而是依赖 onError 里的单点清除。如果需要彻底重置,可取消注释:
// this.clearCache();
// 4. 强制重置加载锁
this.isLoading = false;
this.loadingPromise = null;
this.currentLoadingIndex = 0;
// 5. 重新初始化 (initAudios 会自增 loadGeneration,从而终止之前的任何异步循环)
return this.initAudios();
}
}
// 导出单例
export default new AudioManager();