ios浏览器息屏及前后台切换音频播放问题

工程信息:
CocosCreator2.4.13 helloword 工程中额外添加一个按钮点击进行音频播放,并监听播放完成事,打包web工程进行测试。
demo 下载地址 https://imonkey-courseware-prod.oss-cn-beijing.aliyuncs.com/xuexiong_test/webTest/music-ios-test.zip
也可直接扫二维码测试

问题及操作描述:
版本信息:ipad6 os版本 17.7.5
操作一:safari 浏览器多 tab 页切换,声音重叠问题
表现:可以正常收到前后台切换事件,以及音频播放完成回调,前后台切换一次后,再次点击 play 按钮进行播放则会出现声音重叠的情况,后需再切换再点击按钮表现正常。


操作二:播放音频中息屏后再打开
表现:首次可以收到音频播放完毕回调,再次点击按钮播放音频,再次息屏再打开收不到回调
此时多次点击播放按钮无响应,再次息屏再打开恢复收音播放,收到多个回调。
操作三:将 safari 切到后台,再切回
表现: 声音播放回调未调用,再点击播放按钮,有日志输出,音频系统无响应,只能刷新页面

其他版本信息:
ios12.3.1 表现正常
ios 17.5.1 ios 18.4.1 表现同上述操作三,未假死状态。
貌似 ios 13 以下没问题。
我们的备选方案是 ios 端提供音频播放,上层调用,但是这个对于老项目成本太高了,希望有处理过这个问题的大佬分享一下解决方案,另外 cocosCreator 3.x 的版本也有这个问题

我在3.x遇到过这些问题,你可以看看有没有帮助对你
3.x web端播放音频问题

感谢分享,我看下你的3.x 的帖子, 我之前也尝试,在前台恢复 AudioContext 的状态,并尝试恢复之前正在播放的音频,大致代码如下
onResume() {
const globalContext = cc.sys.__audioSupport.context;
if (!globalContext) {
console.warn(“找不到全局AudioContext”);
return;
}

    console.log("当前AudioContext状态:", globalContext.state);
    
    if (globalContext.state === 'suspended' || globalContext.state === 'interrupted') {
        console.log("恢复全局AudioContext");
        
        // 尝试恢复全局AudioContext
        globalContext.resume().then(() => {
            console.log("全局AudioContext恢复成功");
            
            // 强制"唤醒"音频系统
            this.forceResumeAudioContext();
            
            // 恢复正在播放的音频
            this.resumePlayingAudio();
        }).catch(err => {
            console.error("恢复AudioContext失败:", err);
            
            // 尝试使用更激进的方式恢复
            this.forceResumeAudioContext();
        });
    } else {
        console.log("AudioContext状态已正常:", globalContext.state);
        
        // 即使状态看起来正常,也进行一次音频恢复
        // 有时iOS会报告状态正常但实际上还是无法播放
        this.resumePlayingAudio();
    }
}

forceResumeAudioContext() {
    try {
        const globalContext = cc.sys.__audioSupport.context;
        if (!globalContext) return;
        
        // 创建一个短暂的空音频来"唤醒"系统
        const testOsc = globalContext.createOscillator();
        const testGain = globalContext.createGain();
        testGain.gain.value = 0.01; // 极小音量但不是0
        testOsc.connect(testGain);
        testGain.connect(globalContext.destination);
        testOsc.start(0);
        setTimeout(() => {
            try {
                testOsc.stop();
                testOsc.disconnect();
                testGain.disconnect();
            } catch (e) {}
        }, 100); // 短暂播放100ms
        
        console.log("强制唤醒AudioContext完成");
    } catch (e) {
        console.error("强制唤醒AudioContext失败:", e);
    }
}

实际测试并不是 100%能够恢复的,ios 17.7.5 测试恢复率在 90% ios 18.4.1 恢复率不到一半, 也尝试过不走恢复的方案,而是重建整个 AudioContext 并把旧的关闭,使用新的,也是不行, 区别是恢复的逻辑,一旦没有恢复,点击按钮进行播放音频是播放不出来的, 重建的可以, 也跟切到后台的时长有关系,总之目前还是没有根本解决

完整代码


const { ccclass, property } = cc._decorator;

@ccclass
export default class NewClass extends cc.Component {

    private _resumeAttempts = 0;
    private _maxResumeAttempts = 3;
    private _resumeTimer = null;

    onLoad() {
        
        // 使用自定义的展示事件处理函数
        cc.game.on(cc.game.EVENT_SHOW, this.onGameShow.bind(this));
        cc.game.on(cc.game.EVENT_HIDE, this.onGameHide.bind(this));
    }

    onGameShow() {
        console.log("Game.EVENT_SHOW");
        if (cc.sys.os === cc.sys.OS_IOS) {
            // 清除之前可能存在的定时器
            this.clearResumeTimer();
            
            // 重置恢复尝试次数
            this._resumeAttempts = 0;
            
            // 立即尝试一次恢复
            this.onResume();
            
            // 设置多次尝试恢复的定时器
            this.scheduleResumeAttempts();
        }
    }

    onGameHide() {
        console.log("Game.EVENT_HIDE");
        // 清除恢复定时器
        this.clearResumeTimer();
    }

    scheduleResumeAttempts() {
        // 设置多次尝试,间隔递增
        this._resumeTimer = setTimeout(() => {
            this._resumeAttempts++;
            if (this._resumeAttempts < this._maxResumeAttempts) {
                console.log(`第${this._resumeAttempts}次尝试恢复AudioContext`);
                this.onResume();
                this.scheduleResumeAttempts();
            }
        }, 300 * (this._resumeAttempts + 1)); // 300ms, 600ms, 900ms递增间隔
    }

    clearResumeTimer() {
        if (this._resumeTimer) {
            clearTimeout(this._resumeTimer);
            this._resumeTimer = null;
        }
    }

    @property(cc.AudioClip)
    audioClip: cc.AudioClip = null;

    start() {}

    onDestroy() {
        // 清除定时器
        this.clearResumeTimer();
        
        // 移除事件监听
        window.removeEventListener('pageshow', this.onPageShow.bind(this), true);
        cc.game.off(cc.game.EVENT_SHOW, this.onGameShow, this);
        cc.game.off(cc.game.EVENT_HIDE, this.onGameHide, this);
    }

    play() {
        // 在播放前检查AudioContext状态
        if (cc.sys.os === cc.sys.OS_IOS) {
            const globalContext = cc.sys.__audioSupport.context;
            if (globalContext && 
                (globalContext.state === 'suspended' || globalContext.state === 'interrupted')) {
                
                console.log("播放前恢复AudioContext");
                globalContext.resume().then(() => {
                    this.doPlay();
                }).catch(err => {
                    console.error("播放前恢复AudioContext失败:", err);
                    // 尝试强制恢复
                    this.forceResumeAudioContext();
                    setTimeout(() => this.doPlay(), 100);
                });
            } else {
                this.doPlay();
            }
        } else {
            this.doPlay();
        }
    }

    doPlay() {
        const audioId = cc.audioEngine.playEffect(this.audioClip, false);
        console.log("audioId play:", audioId);
        if (audioId) {
            cc.audioEngine.setVolume(audioId, 1);
            cc.audioEngine.setEffectsVolume(1);
            cc.audioEngine.setFinishCallback(audioId, () => {
                console.log("audio play finish");
            });
        } else {
            console.error("音频播放失败,无效的audioId");
        }
    }

    onResume() {
        const globalContext = cc.sys.__audioSupport.context;
        if (!globalContext) {
            console.warn("找不到全局AudioContext");
            return;
        }
        
        console.log("当前AudioContext状态:", globalContext.state);
        
        if (globalContext.state === 'suspended' || globalContext.state === 'interrupted') {
            console.log("恢复全局AudioContext");
            
            // 尝试恢复全局AudioContext
            globalContext.resume().then(() => {
                console.log("全局AudioContext恢复成功");
                
                // 强制"唤醒"音频系统
                this.forceResumeAudioContext();
                
                // 恢复正在播放的音频
                this.resumePlayingAudio();
            }).catch(err => {
                console.error("恢复AudioContext失败:", err);
                
                // 尝试使用更激进的方式恢复
                this.forceResumeAudioContext();
            });
        } else {
            console.log("AudioContext状态已正常:", globalContext.state);
            
            // 即使状态看起来正常,也进行一次音频恢复
            // 有时iOS会报告状态正常但实际上还是无法播放
            this.resumePlayingAudio();
        }
    }
    
    forceResumeAudioContext() {
        try {
            const globalContext = cc.sys.__audioSupport.context;
            if (!globalContext) return;
            
            // 创建一个短暂的空音频来"唤醒"系统
            const testOsc = globalContext.createOscillator();
            const testGain = globalContext.createGain();
            testGain.gain.value = 0.01; // 极小音量但不是0
            testOsc.connect(testGain);
            testGain.connect(globalContext.destination);
            testOsc.start(0);
            setTimeout(() => {
                try {
                    testOsc.stop();
                    testOsc.disconnect();
                    testGain.disconnect();
                } catch (e) {}
            }, 100); // 短暂播放100ms
            
            console.log("强制唤醒AudioContext完成");
        } catch (e) {
            console.error("强制唤醒AudioContext失败:", e);
        }
    }
    
    resumePlayingAudio() {
        // 遍历所有音频并检查它们的状态
        const audioIds = Object.keys(cc.audioEngine._id2audio);
        console.log("恢复音频状态检查, 当前音频数:", audioIds.length);
        
        if (audioIds.length === 0) return;
        
        // 恢复所有正在播放的音频
        for (let id of audioIds) {
            const audio = cc.audioEngine._id2audio[id];
            if (!audio || !audio._element) continue;
            
            const audioState = audio._state;
            console.log(`音频ID: ${id}, 状态: ${audioState}`);
            
            if (audioState === cc.audioEngine.AudioState.PLAYING && 
                (audio._element.paused || 
                (audio._element._currentSource === null && audio._element._context))) {
                console.log(`恢复播放音频: ${id}`);
                
                try {
                    // 先确保音量正确
                    const volume = audio.getVolume();
                    audio.setVolume(volume);
                    
                    // 然后恢复播放
                    audio.resume();
                } catch (e) {
                    console.error(`恢复音频${id}失败:`, e);
                }
            }
        }
    }
}


这样可以不