iPhone X Safari首触触发音频播放报NotAllowedError错误如何解决
问题原因
- iOS Safari对HTMLAudioElement的播放权限严格限制在用户交互事件(pointerdown/click/tap等)的同步执行上下文内,异步回调(包括await后的微任务、setTimeout等)中的play调用都会被拦截,你的代码中将play逻辑放在了
await ctx.resume()之后,已经脱离了用户交互的同步栈,所以触发权限错误。 - Web Audio Context和HTMLAudioElement的播放权限是独立管控的,你唤醒AudioContext的操作对HTMLAudioElement的播放没有任何作用,这也是为什么你这套逻辑对背景音无效。
- 你的音效可以正常播放,是因为音效的play调用通常在其他用户交互的同步回调中触发,符合Safari的权限要求。
修复方案
核心改动两点:
- 移除不必要的AudioContext权限唤醒逻辑,在首次用户交互的同步栈内直接激活所有HTMLAudioElement的播放权限
- 给play调用添加错误捕获,避免未处理的Promise报错
修改后代码
import App from "../../../app"; import * as createjs from "createjs-module"; export default class AudioManager { private map: Map<string, Sound>; private pointerDownHandler: () => void; constructor(sounds:Map<string, HTMLAudioElement>) { this.map = new Map<string, Sound>(); sounds.forEach((value, key) => { this.map.set(key, new Sound(key, value)); value.pause(); // 提前设置好循环属性,避免后续异步设置出问题 if (key === "assets/sounds/soundtrack.mp3") { value.loop = true; } }); this.pointerDownHandler = this.requestAudioPermission; window.addEventListener("pointerdown", this.pointerDownHandler); } // 去掉async,所有逻辑走同步执行 requestAudioPermission = () => { this.map.forEach((value) => { // 同步调用一次play再立刻pause,激活当前音频的播放权限 value.tryActivate(); value.givePermissionToPlay(); }); // 直接同步调用背景音播放,不要放在异步逻辑后 this.getAudio("assets/sounds/soundtrack.mp3").tryPlay(); window.removeEventListener("pointerdown", this.pointerDownHandler); }; getAudio(key: string): Sound { return this.map.get(key) as Sound; } fadeMusic(sound: Sound, to: number) { sound.fade(to); } mute() { this.map.forEach((map) => { map.setMuted(true); }); } unmute() { this.map.forEach((map) => { map.setMuted(false); }); } } export class Sound { private name: string; private audioElement: HTMLAudioElement; private hasPermissionToPlay = false; public get muted(): boolean { return this.audioElement.muted; } constructor(name: string, audioElement: HTMLAudioElement) { this.name = name; this.audioElement = audioElement; } // 新增权限激活方法 tryActivate() { // 静音播放一次立刻暂停,用户无感知同时激活权限 const originVolume = this.audioElement.volume; this.audioElement.volume = 0; this.audioElement.play() .then(() => this.audioElement.pause()) .catch(() => {}) .finally(() => { this.audioElement.volume = originVolume; }); } tryPlay() { if (this.hasPermissionToPlay) { // 捕获play的promise错误,避免未处理的promise rejection报错 this.audioElement.play().catch(() => {}); console.log(this.name); } } pause() { this.audioElement.pause(); } setMuted(mute: boolean) { this.audioElement.muted = mute; } setVolume(volume: number) { this.audioElement.volume = volume; } setCurrentTime(time: number) { this.audioElement.currentTime = time; } setLoop(loop: boolean) { this.audioElement.loop = loop; } fade(to: number) { createjs.Tween.get(this.audioElement).to({ volume: to }, 500); } givePermissionToPlay() { this.hasPermissionToPlay = true; } }
注意事项
- 传入AudioManager的所有HTMLAudioElement需要提前设置好src属性并完成预加载,不要在用户交互触发时才动态赋值src
- 如果业务场景需要使用Web Audio API播放音频,就不要混用HTMLAudioElement,统一把音频解码为AudioBuffer用AudioContext播放,只需要在首次用户交互时唤醒一次AudioContext即可全局生效
内容的提问来源于stack exchange,提问作者anonymous-dev
相关产品推荐
相关产品推荐

