关于audio自动播放的一个小坑

5,638 阅读4分钟

背景

在日常需求开发中,遇到一个这样的诉求,用户在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。

收获总结

  • 接需求需谨慎,方案需调研
  • 容器能力强
  • 还有其他坑??。。。在坑中慢慢成长