703 lines
23 KiB
JavaScript
703 lines
23 KiB
JavaScript
export const audioFils = {
|
||
// 激光已校准:
|
||
// "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/attachment/2025-09-17/dcutya55ufiiw8oo55.mp3",
|
||
未上靶:
|
||
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6n45o3tsm1v4unam.mp3",
|
||
"1环":
|
||
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin1aq7gxjih5l.mp3",
|
||
"2环":
|
||
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin64tdgx2s4at.mp3",
|
||
"3环":
|
||
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxinlmf87vt8z65.mp3",
|
||
"4环":
|
||
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxinniv97sx0q9u.mp3",
|
||
"5环":
|
||
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin7j01kknpb7k.mp3",
|
||
"6环":
|
||
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin4syy1015rtq.mp3",
|
||
"7环":
|
||
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin3iz3dvmjdai.mp3",
|
||
"8环":
|
||
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxinnjd42lhpfiw.mp3",
|
||
"9环":
|
||
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin69nj1xh7yfz.mp3",
|
||
"10环":
|
||
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxinnvsx0tt7ksa.mp3",
|
||
X环: "https://static.shelingxingqiu.com/attachment/2026-02-09/dga8puwekpe2gmtbu4.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",
|
||
};
|
||
|
||
// 版本控制日志函数
|
||
function debugLog(...args) {
|
||
// 获取当前环境信息
|
||
const accountInfo = uni.getAccountInfoSync();
|
||
const envVersion = accountInfo.miniProgram.envVersion;
|
||
|
||
// 只在体验版打印日志,正式版(release)和开发版(develop)不打印
|
||
if (envVersion === "trial") {
|
||
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.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.initAudios();
|
||
}
|
||
|
||
// 清理不再使用的缓存文件
|
||
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);
|
||
try {
|
||
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);
|
||
// 如果是本地文件加载失败,可能是文件损坏,清除缓存以便下次重新下载
|
||
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);
|
||
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);
|
||
};
|
||
|
||
// 检查是否有可用的本地缓存
|
||
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);
|
||
if (callback) callback();
|
||
}
|
||
},
|
||
fail: () => {
|
||
this.recordLoadFailure(key);
|
||
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);
|
||
}
|
||
|
||
// 重新加载音频
|
||
retryLoadAudio(key) {
|
||
const oldAudio = this.audioMap.get(key);
|
||
if (oldAudio) oldAudio.destroy();
|
||
this.createAudio(key);
|
||
}
|
||
|
||
// 播放指定音频或音频数组(数组则按顺序连续播放)
|
||
play(input, interrupt = true) {
|
||
// 统一规范化为队列
|
||
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) {
|
||
// 200ms 内的同 key 重复播放直接忽略,避免“比比赛开始”这类重复首音
|
||
const now = Date.now();
|
||
if (this.lastPlayKey === key && now - this.lastPlayAt < 250) {
|
||
debugLog(`忽略快速重复播放: ${key}`);
|
||
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);
|
||
|
||
audio.play();
|
||
this.currentPlayingKey = key;
|
||
this.lastPlayKey = key;
|
||
this.lastPlayAt = Date.now();
|
||
} else {
|
||
debugLog(`音频 ${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 (_) {}
|
||
}
|
||
}
|
||
|
||
// 连续播放:在某个音频结束后,若处于队列播放状态则继续下一个
|
||
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);
|
||
if (audio) {
|
||
audio.stop();
|
||
this.allowPlayMap.set(key, false);
|
||
if (this.currentPlayingKey === key) {
|
||
this.currentPlayingKey = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 停止所有音频
|
||
stopAll() {
|
||
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.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();
|