背景
在日常需求开发中,遇到一个这样的诉求,用户在A页面,点击具体的某一首歌曲,跳转至B页面,要求携带该首歌曲的ID,在B页面能够自动播放该首歌曲。
页面使用audio
标签:
// react 环境
<audio
ref={this.audioRef}
src={playingMusicSource}
autoPlay
loop
/>
// 根据URL上带的歌曲ID信息,播放该首歌曲
this.audioRef.current.play();
自测阶段,用自己的iPhone测了下发现一切正常,觉得很完美😄
交付给PM,PM验收时发现,歌曲进入B页面后未自动播放😭
问题定位
问了下PM使用的是安卓手机,(由于疫情在家办公),手边无安卓机,于是使用Chrome先尝试复现问题。打开控制台,发现如下错误👇:

点击错误提示中给出的链接,发现这是Google的一个严格自动播放策略,简单来说就是Google处于用户体验角度的考虑,在用户没有任何交互或者是预先做一些设置的情况下,不允许自动播放。
当然也有一些可允许自动播放的场景:(前提还是要让用户做些动作)

解决
Chrome浏览器中:
发现这个策略以后很绝望,但细想了下,既然Google是出于用户体验角度制定了这个规则,那是不是PM的需求不合理?
- 解法一:跟PM沟通下,到B页面增加一个交互?
跟PM简单介绍了下Google的相关策略,以及背后的关于用户体验的考虑。PM表示理解,BUT 实际场景是,A页面已经由于法务问题不让播放,预期是到B页面播放的,还要再增加一个交互,对于歌曲的播放路径来说太长了。
实际结果:我没把PM说服,PM把我说服了。😅
沟通结果是,我们两侧都在想想怎么更合适。
- 解法二:网易等音乐播放器都可自动播,咋做的?
查看了蛮多方法(网上还挺多的),有些看起来靠谱(比如定时轮训),但实测下来不可行(也许是操作姿势不对)。
由于时间紧急,大晚上的。。。翻到一个简易的方法,使用 <iframe>。
测试了下,发现iframe可以自动播放,并没有限制。
实际改造
接下来就是改造环节,由于 <iframe>并不是一个非常理想的播放设备,所以此处只将其用作一次性的播放器。
比较简单:就是在body上创建一个iframe,动态添加上去。
if (X) { // X为一些需要自动播放的判断条件
const musicInfo = musicList.find(item => item.id_str === currentMusicId) || {};
const newUri = get(musicInfo, 'play_url.url_list[0]');
this.iframeNode = document.createElement('iframe');
this.iframeNode.setAttribute('src', newUri);
this.iframeNode.setAttribute('allow', 'autoplay');
this.iframeNode.setAttribute('hidden', true); // 不在界面上显示
// 3000为延迟时间,实测下来播放需要一定的下载时间,因此停止的图标需要延迟关闭
const time = Number(musicInfo.duration) * 1000 + 3000;
// 因为iframe 无法监听播放结束,也无法loop播放,
// 因此设置定时器,在歌曲播放结束后,将播放的图标切换为 停止🤚图标
this.iframeNodeTimer = setTimeout(() => {
this.pauseMusicPlay();
this.clearIframeNodeTimer(); // 清理定时器
this.clearIframeNode();
}, time);
// 将创建的element元素,添加到body上。
document.body.appendChild(this.iframeNode);
this.setState({
musicPaused: false,
playingMusicSource: newUri,
});
}
实际场景中,还有一些问题:
- 用户进入页面后,歌曲还未播放完成,用户就暂停正在播放的歌曲
- 用户进入页面后,歌曲还未播放完成,用户就点击播放其他歌曲
- 用户进入页面后,歌曲已经播放完成,用户播放任一歌曲
由于首次进入使用的是iframe播放,所以在用户点击暂停⏸️,以及切换播放的歌曲需要额外的处理。
在用户点击歌曲封面以后,会响应handlePlayMusic
事件,需要在这个事件中,对于创建的iframe元素做些额外的处理。
// 清理定时器
clearIframeNodeTimer = () => {
if (this.iframeNodeTimer) {
this.iframeNodeTimer && clearTimeout(this.iframeNodeTimer);
this.iframeNodeTimer = null;
}
}
// 清理iframe结点
clearIframeNode = () => {
if (this.iframeNode) {
document.body.removeChild(this.iframeNode);
this.iframeNode = null;
}
}
handlePlayMusic = idStr => {
// 判断当前界面是否存在this.iframeNode
// this.iframeNode存在:则说明当前第一首歌还未播放完成,
// 此时应该先响应【暂停】动作,停止当前歌曲的播放
// 若用户当前点击的是其他歌曲,则播放其他歌曲
if (this.iframeNode) {
document.body.removeChild(this.iframeNode);
this.iframeNode = null;
this.pauseMusicPlay();
if (newUri !== playingMusicSource) {
const currentIndex = musicList.findIndex(item => item.id_str === idStr);
this.togglePlayer({ action: 'play', uri: newUri, musicId: idStr, order: currentIndex, tabName: this.tabName });
}
return;
}
// 其他正常播放逻辑
}
对于上面的3个可能的场景:以this.iframeNode
是否存在作为【第一首歌曲是否播放完成的标志】 。存在则需对 this.iframeNode
特殊处理,如果不存在,则正常处理即可。
Android端内
需要借助于端能力,将端内的 setMediaPlaybackRequiresUserGesture(默认为true),设置为false。
收获总结
- 接需求需谨慎,方案需调研
- 容器能力强
- 还有其他坑??。。。在坑中慢慢成长