浏览器最小化后音频文件无法播放的解决方案?

情形一:浏览器未进行过声音文件的播放,直接最小化,然后触发去播放声音文件,声音文件无法播放。此时再将浏览器放大,触发播放声音文件的方法,声音正常播放。
情形二:浏览器最大化且进行过声音文件的播放,然后最小化,再去触发播放声音文件,可以正常播放。
代码如下

canplaythroughListener;

init(fileName: string) {
    let fileUrl = '/' + fileName
    this._source.setAttribute("src", fileUrl)
    // 再次加载相同的音频文件--重载
    this._audio.load()
    this._audio.autoplay = true;
    return new Promise((resolve, reject) => {
        this.canplaythroughListener = () => {
            console.log('this._audio.readyState', this._audio.readyState);
            try {
                if (this._audio.readyState == this._audio.HAVE_ENOUGH_DATA) {
                    this._audio.play()
                    .then(()=>{
                        this._audio.play()
                        console.log('this._audio', this._audio)
                        resolve(undefined)
                    })
                    .catch((err)=>{
                        if (err.name == 'NotAllowedError') {
                            console.log('NotAllowedError')
                            reject()
                        }
                    })
                }
            } catch (error) {
                console.log('catch-error')
            }
        };
        this._audio.addEventListener("canplaythrough", this.canplaythroughListener);
        // test
        this._audio.addEventListener('load', () => {
            console.log('load~~~~')
            this._audio.addEventListener('canplaythrough', function() {
                console.log('Video can play through');
            });
        });
         
        this._audio.addEventListener('error', function() {
            console.log('Error loading video');
        });
    })  
}

测试结果如下:当处于情形一的时候,audio对象的addEventListener事件未添加成功。当处于情形二的时候,addEventListener事件添加成功,内部方法也可正常执行。

猜测:声音只要正常播放过,全局即存在this._audio.addEventListener("canplaythrough"),后续哪怕浏览器最小化,也可以正常播放。

请问造成浏览器初始化状态未最小化且未进行过声音文件的播放,执行声音文件的播放方法且无法播放的原因是什么,有什么解决方案?

阅读 395
avatarAI BotBETA

问题原因分析:

  1. 浏览器自动播放策略限制
    现代浏览器(Chrome >=66、Safari等)对音频自动播放有严格限制。当以下条件同时满足时,音频会被阻止:

    • 页面未收到任何用户手势(点击、触摸等)
    • 浏览器处于后台状态(最小化/切换标签页)
    • 首次播放音频时没有用户交互
  2. 事件监听时机问题
    在情形一中,当浏览器最小化时:

    this._audio.addEventListener("canplaythrough", this.canplaythroughListener)

    可能由于浏览器节流机制,导致事件监听器未能及时挂载

  3. play()调用
    代码中存在嵌套的this._audio.play()调用:

    this._audio.play().then(() => {
        this._audio.play()  // 重复调用
    })

    这可能导致NotAllowedError(当浏览器认为这是非用户触发的自动播放时)


解决方案:

方案一:遵循自动播放策略(推荐)

// 增加用户手势监听
document.addEventListener('click', initAudioContext, { once: true })

async function initAudioContext() {
    // 在用户交互后初始化音频
    await this._audio.play().catch(e => console.log('预备播放失败'))
    this._audio.pause()
    this._audio.currentTime = 0
}

方案二:优化事件监听逻辑

// 修改后的 init 方法
init(fileName: string) {
    // 清除旧监听器防止重复
    this._audio.removeEventListener("canplaythrough", this.canplaythroughListener)
    
    // 使用一次性监听器
    const canPlayHandler = () => {
        this._audio.removeEventListener("canplaythrough", canPlayHandler)
        this.handlePlay().then(resolve).catch(reject)
    }
    this._audio.addEventListener("canplaythrough", canPlayHandler)
}

private async handlePlay() {
    try {
        // 单次播放调用
        await this._audio.play()
        console.log('Playback started')
    } catch (err) {
        if (err.name === 'NotAllowedError') {
            console.warn('需要用户交互后才能播放')
            // 可在此处添加点击按钮提示
        }
    }
}

方案三:使用 Web Audio API(更底层控制)

const audioContext = new (window.AudioContext || window.webkitAudioContext)()
const source = audioContext.createMediaElementSource(this._audio)
source.connect(audioContext.destination)

// 恢复音频上下文(需要用户手势)
document.addEventListener('click', () => {
    if (audioContext.state === 'suspended') {
        audioContext.resume()
    }
})

关键优化点:

  1. 用户手势优先:在页面首次加载时,通过按钮或其他UI元素要求用户点击激活音频
  2. 错误处理优化:捕获NotAllowedError并提供用户反馈
  3. 资源管理

    // 页面可见性变化处理
    document.addEventListener('visibilitychange', () => {
        if (document.hidden) {
            this._audio.pause()
        } else {
            this._audio.play().catch(() => {})
        }
    })
  4. 预加载策略

    <!-- 添加 preload 元数据 -->
    audio preload="auto"></audio>

遵循这些修改后,当浏览器从最小化状态恢复时,通过visibilitychange事件触发重新播放,配合用户手势验证,可解决初始化状态下的播放问题。

1 个回答
init(fileName: string) {
    let fileUrl = '/' + fileName
    this._source.setAttribute("src", fileUrl)
    this._audio.load()
    
    document.addEventListener('visibilitychange', () => {
      if (!document.hidden && this._audio.paused) {
        this._audio.play().catch(err => console.log('可见性变化时播放失败:', err));
      }
    });
    
    return new Promise((resolve, reject) => {
        // ... existing code ...
        this.canplaythroughListener = () => {
            console.log('this._audio.readyState', this._audio.readyState);
            try {
                if (this._audio.readyState == this._audio.HAVE_ENOUGH_DATA) {
            
                    if (!document.hidden) {
                        this._audio.play()
                        .then(()=>{
                            console.log('音频播放成功');
                            resolve(undefined);
                        })
                        .catch((err)=>{
                            console.log('播放错误:', err);
                            if (err.name == 'NotAllowedError') {
                                // 存储播放意图,等待用户交互
                                this._pendingPlay = true;
                                console.log('需要用户交互才能播放');
                                // 不立即reject,等待用户交互
                            } else {
                                reject(err);
                            }
                        });
                    } else {
                        // 页面不可见,标记为待播放
                        this._pendingPlay = true;
                        console.log('页面不可见,等待页面可见后播放');
                    }
                }
            } catch (error) {
                console.log('catch-error', error);
                reject(error);
            }
        };
        // ... existing code ...
        
        //监听器
        const userInteractionHandler = () => {
            if (this._pendingPlay) {
                this._audio.play()
                .then(() => {
                    this._pendingPlay = false;
                    resolve(undefined);
                })
                .catch(err => console.log('用户交互后播放失败:', err));
            }
        };
        
        ['click', 'touchend', 'keydown'].forEach(eventType => {
            document.addEventListener(eventType, userInteractionHandler, {once: true});
        });
    });  
}
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏