简单记录一下一个小问题。
背景:h5 hybrid 应用,Vue 项目
需求:App 有背景音乐,h5 页面也有音频播放,当进入页面时设置 App 音频静音,离开页面时设置 App 非静音。
实现代码:
import { showToast } from 'vant' import { setAudioState } from '@/apis/base' export function useMusicPlayer(audioElementRef: Ref<HTMLAudioElement | null>): { isPlaying: Ref<boolean> togglePlay: () => void } { const isPlaying = ref(false) /** * @function togglePlay * @description 切换音乐播放暂停 * @returns {void} */ function togglePlay(): void { const audioElement = audioElementRef.value if (audioElement) { if (isPlaying.value) { audioElement.pause() } else { // 静音播放,浏览器可能允许 audioElement.muted = true void audioElement.play().catch((error) => { console.log('music player error: ', error) showToast('音乐播放受限,点击页面以允许播放') }) audioElement.muted = false // 取消静音,用户交互后 } isPlaying.value = !isPlaying.value } } /** * @function handleVisibilityChange * @description 处理页面可见性事件 * @returns {void} */ function handleVisibilityChange(): void { if (document.hidden) { if (isPlaying.value) { togglePlay() } } } /** * @function setAppAudioState * @description 设置音频状态 * @param mute 是否静音 */ function setAppAudioState(mute: boolean): void { setAudioState({ mute, }).catch((error) => { showToast(error.message) }) } onMounted(() => { document.addEventListener('visibilitychange', handleVisibilityChange) setAppAudioState(true) }) onBeforeUnmount(() => { document.removeEventListener('visibilitychange', handleVisibilityChange) audioElementRef.value?.pause() audioElementRef.value = null setAppAudioState(false) }) return { isPlaying, togglePlay, } }
其中 setAudioState
是和原生 App 的通信,设置静音或取消静音。一切都没有问题。
发现的问题:
当我尝试不是在进入页面/离开页面(onMounted/onBeforeUnmount)时设置 App 静音,而是在点击播放/暂停 Web 音频时进行。代码如下:
import { showToast } from 'vant' import { setAudioState } from '@/apis/base' export function useMusicPlayer(audioElementRef: Ref<HTMLAudioElement | null>): { isPlaying: Ref<boolean> togglePlay: () => void } { const isPlaying = ref(false) /** * @function togglePlay * @description 切换音乐播放暂停 * @returns {void} */ function togglePlay(): void { const audioElement = audioElementRef.value if (audioElement) { if (isPlaying.value) { setAppAudioState(false) audioElement.pause() } else { setAppAudioState(true) // 静音播放,浏览器可能允许 audioElement.muted = true void audioElement.play().catch((error) => { console.log('music player error: ', error) showToast('音乐播放受限,点击页面以允许播放') }) audioElement.muted = false // 取消静音,用户交互后 } isPlaying.value = !isPlaying.value } } /** * @function handleVisibilityChange * @description 处理页面可见性事件 * @returns {void} */ function handleVisibilityChange(): void { if (document.hidden) { if (isPlaying.value) { togglePlay() } } } /** * @function setAppAudioState * @description 设置游戏音频状态 * @param mute 是否静音 */ function setAppAudioState(mute: boolean): void { setAudioState({ mute, }).catch((error) => { showToast(error.message) }) } onMounted(() => { document.addEventListener('visibilitychange', handleVisibilityChange) }) onBeforeUnmount(() => { document.removeEventListener('visibilitychange', handleVisibilityChange) audioElementRef.value?.pause() audioElementRef.value = null }) return { isPlaying, togglePlay, } }
此时就出现了问题,Web 音频 play 时,能正常设置 App 静音(setAppAudioState(true)
),Web 音频 pause 时,不能恢复 App 音量,也就是 setAppAudioState(false)
似乎不起作用。
于是联调,发现这句代码确实执行了,App 也确实收到了信息。而且:
安卓没有问题,暂停能恢复 App 音频设置
iPad Air 2019 也没有问题
我测试的有问题的手机是 iPhone 16。
问了一下 ChatGPT,提到了异步相关的问题,setAudioState
返回是一个 Promise,但是 setAppAudioState
的异步执行和 audioElement.pause()
应该相互不影响啊
我将 setAppAudioState(false)
放在定时器中延时执行:
setTimeout(() => { setAppAudioState(false) }, 100)
此时的现象是:Web 音频播放,App 静音;Web 音频暂停,延时一会,App 恢复声音,持续1-2s,App 静音。
很神奇的现象!
我尝试增加延时时长到,1s、2s,恢复音量无效。延时 3s 时才能正常恢复 App 的音量。
系统音频管理
iOS 对音频流的管理非常严格,尤其是在涉及多个音频源时。系统可能会检测到并管理不同的音频源以优化性能和用户体验。例如,当多个音频源同时播放时,系统可能会优先暂停或降低某些音频的音量。
如果 Web 应用和原生应用都在输出音频,系统可能会对其中一个进行调整,这可能解释了在暂停 Web 音频后,App 音量恢复后又很快消失的问题。
音频焦点
iOS 使用音频焦点来管理音频输出,这意味着当一个应用获取音频焦点时,其他音频可能会被暂停或停止。确认你的应用是否正确处理了音频焦点的获取和释放。
系统 bug 或特性
总之,这个问题,因为不熟悉底层的系统音频播放管理,还不能理解。暂时一记。