|
@@ -11,6 +11,12 @@ class AudioManager {
|
|
|
|
|
|
|
|
// 🎯 全局默认播放次数(你可以根据需要改为 1 或 3)
|
|
// 🎯 全局默认播放次数(你可以根据需要改为 1 或 3)
|
|
|
this.defaultRepeatCount = 3; // 默认循环3次
|
|
this.defaultRepeatCount = 3; // 默认循环3次
|
|
|
|
|
+
|
|
|
|
|
+ // 被浏览器策略拦截后待重试的播放队列
|
|
|
|
|
+ this._pendingPlaybacks = [];
|
|
|
|
|
+
|
|
|
|
|
+ // 全局用户手势监听(用于重试被拦截的播放)
|
|
|
|
|
+ this._setupUserGestureRetry();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -26,6 +32,36 @@ class AudioManager {
|
|
|
console.log('🛑 已停止所有音频');
|
|
console.log('🛑 已停止所有音频');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 恢复 AudioContext(绕过浏览器自动播放限制)
|
|
|
|
|
+ */
|
|
|
|
|
+ _resumeAudioContext() {
|
|
|
|
|
+ if (this._audioContext && this._audioContext.state === 'suspended') {
|
|
|
|
|
+ this._audioContext.resume().catch(() => {});
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 设置用户手势后重试被拦截的播放
|
|
|
|
|
+ */
|
|
|
|
|
+ _setupUserGestureRetry() {
|
|
|
|
|
+ const retryAll = () => {
|
|
|
|
|
+ if (!this._pendingPlaybacks.length) return;
|
|
|
|
|
+ const tasks = [...this._pendingPlaybacks];
|
|
|
|
|
+ this._pendingPlaybacks = [];
|
|
|
|
|
+ tasks.forEach((fn) => fn());
|
|
|
|
|
+ };
|
|
|
|
|
+ const handler = () => {
|
|
|
|
|
+ retryAll();
|
|
|
|
|
+ document.removeEventListener('click', handler);
|
|
|
|
|
+ document.removeEventListener('touchstart', handler);
|
|
|
|
|
+ document.removeEventListener('keydown', handler);
|
|
|
|
|
+ };
|
|
|
|
|
+ document.addEventListener('click', handler, { once: true });
|
|
|
|
|
+ document.addEventListener('touchstart', handler, { once: true });
|
|
|
|
|
+ document.addEventListener('keydown', handler, { once: true });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 播放音频(自动停止之前的音频,并支持自定义重复次数)
|
|
* 播放音频(自动停止之前的音频,并支持自定义重复次数)
|
|
|
* @param {string} src - 音频文件路径
|
|
* @param {string} src - 音频文件路径
|
|
@@ -88,10 +124,18 @@ class AudioManager {
|
|
|
onEnded,
|
|
onEnded,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ // 播放前先尝试恢复 AudioContext(绕过浏览器自动播放限制)
|
|
|
|
|
+ this._resumeAudioContext();
|
|
|
|
|
+
|
|
|
// 开始播放
|
|
// 开始播放
|
|
|
audio.play().catch((err) => {
|
|
audio.play().catch((err) => {
|
|
|
- // 静音状态下一般不会报错,但若报错则忽略
|
|
|
|
|
|
|
+ // 被浏览器策略拦截时,保存到待播放队列,等下次用户交互时重试
|
|
|
console.warn('音频播放失败(自动播放策略):', err);
|
|
console.warn('音频播放失败(自动播放策略):', err);
|
|
|
|
|
+ if (err.name === 'NotAllowedError') {
|
|
|
|
|
+ this._pendingPlaybacks.push(() => {
|
|
|
|
|
+ audio.play().catch(() => {});
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
return audio;
|
|
return audio;
|
|
@@ -108,6 +152,14 @@ class AudioManager {
|
|
|
});
|
|
});
|
|
|
sessionStorage.setItem('audioUnlocked', 'true');
|
|
sessionStorage.setItem('audioUnlocked', 'true');
|
|
|
this.hasUnlockedBefore = true;
|
|
this.hasUnlockedBefore = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 解锁后重试被拦截的播放
|
|
|
|
|
+ if (this._pendingPlaybacks.length) {
|
|
|
|
|
+ const tasks = [...this._pendingPlaybacks];
|
|
|
|
|
+ this._pendingPlaybacks = [];
|
|
|
|
|
+ tasks.forEach((fn) => fn());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
console.log('🔊 音频已解除静音');
|
|
console.log('🔊 音频已解除静音');
|
|
|
}
|
|
}
|
|
|
|
|
|