react版音乐播放器的各种兼容问题

由于工作要求,需要制作一个h5页面的音乐播放器,其实如果放在原生做,效果会好很多,其实大多数app的音乐播放器也是原生做的,所以会要求你打开app再去播放音乐,但是有些特殊的情况,还是会用到h5播放器,比如说分享,把音乐分享给其他人,这个时候音乐播放器就是一个链接,这种情况下,能够保证在各个环境内都能够播放,就依赖于浏览器的功能,毕竟无论是qq,微信还是推特,社交软件上都内置了浏览器功能,这就允许分享到各个环境中。

其实音乐播放器在不同环境内是有兼容性问题的,今天我们就来一起看看开发过程中,遇到的一些问题以及如何解决吧。

autoPlay自动播放

首先需要讲清楚,本次播放器是由react编写,react版本是18+

这是产品要求的一个功能,希望页面一打开就能够实现自动播放

但是刚开始就遇到了问题,浏览器内是禁止自动播放的,由于怕打扰用户,所以浏览器都关闭了audio的自动播放功能,例如autoPlay属性

谷歌浏览器禁止播放

https://developer.chrome.com/blog/autoplay/

safari浏览器也禁止播放

https://webkit.org/blog/7734/auto-play-policy-changes-for-macos/

{/* 背景音乐+引导语播放 */}
    <audio
      ref={listBackgroundMusic}
      preload='auto'
      autoPlay
      loop
      className={styles.audioHidden} src={paintList.listBackgroundMusic}
    />

这样一打开页面是没有任何反应的,音频并没有开始播放。

既然属性被控制了,那使用js播放呢?毕竟audio标签有一个play方法能够调用直接播放的

play()播放

代码逻辑如下:

useEffect(() => {
    listBackgroundMusic.current.load() // 首次加载音频
    listBackgroundMusic.current.play() // 首次播放音频
  }, [])

页面依然是没有播放的,并且控制台报错了,这个报错就是说播放失败,由于用户没有点击

仔细观看上面的文章,会发现需要用户于发生交互才能自动播放,想到这里,是不是觉得可以模拟手动点击播放呢?

模拟手动点击播放

<div ref={musicIcon} onClick={handleMusic}>musicPlay</div>


useEffect(() => {
  musicIcon.current.click()
},[])

const handlePlay = () => {
    if (isMusicPlay) {
      setIsMusicPlay(false)
      listBackgroundMusic.current.pause()
    } else {
      setIsMusicPlay(true)
      listBackgroundMusic.current.play()
    }
  }

页面依旧还是不能自动播放,控制台也依旧是报错的,报错的信息跟上面一致

在移动端Ipad的报错信息,会直接显示在页面上

看来浏览器就是防止了各种各样的误操作,导致页面影响用户体验,难道这个功能就实现不了了?

酷狗音乐如何实现

当找不到办法时,就可以看看大厂是怎么实现的,比如酷狗音乐的播放能够实现自动播放吗?

https://m.kugou.com/share/?chain=7UcVd6aBfV2

移动端可以扫码看效果

在网页中打开,我们能够发现,会有一个弹出层,然后点击之后,是未播放状态的。

在安卓的微信环境打开,是这样的,不会自动播放

在安卓的默认浏览器打开是会自动播放的

到这里,我们就能够发现,即使安卓,在不同环境内,也存在兼容问题,当不能够播放时,我们能够如何解决呢?首先就是弹出层提示点击播放,然后就是不能播放的情况下,就播放状态为暂停,提示用户未开始播放。

在ios环境内就更加严格了,webkit是禁止页面自动播放的,所以ios的默认浏览器中,不会自动播放

但是在微信中,又是能够自动播放的

对比:

AndroidIOS
默认浏览器能自动播放不能自动播放
微信环境不能自动播放能自动播放

看到这里,其实音频自动播放就存在很多兼容性问题了,大厂也不能实现统一的自动播放

其实如果除了使用h5的能力,也使用原生的能力,那就不会有这么多限制了,像这种在微信内,能够自动播放的,我觉得大概率是使用到了微信的sdk

微信sdk实现自动播放

果然一查,就有这样的实现

https://juejin.cn/post/7113805660667510798

demo:

https://go.163.com/f2e/20220623_howler/index.html?type=audio

这样确实能够在h5内实现自动播放,但是缺点也很明显,就是只是在微信环境内,用户分享到其他地方打开,也就不能自动播放了

所以最后跟产品商量之后,就只能根据兼容性来看,是否自动播放了,不能自动播放的,就默认暂停状态,需要用户手动点击。

不能自动播放,怎么获取到播放失败的状态呢?

DOM调用播放的函数,会返回一个promise,如果promise的状态是reject,那就是失败的,如果播放成功了,就说resolve状态

自动播放就这样解决了~

接下来的是切歌

产品的要求自然是切换下一首之后,又自动播放了,这个时候,可就有点击事件了,不会被浏览器阻止行为了,但是仍然遇到了问题。

切歌播放被禁止

原因:

https://developer.chrome.com/blog/play-request-was-interrupted/

这两个错误,在切歌频繁的时候就会发生,发生之后,就是这首歌不播放了

情况1

Uncaught (in promise) DOMException: The play() request was interrupted by a new load request.

**

出现的原因,音乐的资源未加载完成,就开始播放了

什么时候资源才加载完成呢?

切换歌曲,显然就是更换audio的src属性值,但是这个属性值,一定就是一个路径,什么时候能够保证,这首歌曲加载完成呢?

我找到了Audio对象(MDN)

https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLAudioElement/Audio

初始化对象之后, 这个对象就加载完成了

// Handle setup when changing tracks
  useUpdateEffect(() => {

    audioRef.current.pause(); // 暂停音乐播放
    audioRef.current = new Audio(musicList[curIndex].musicUrl); // 加载另一首音乐(使用对象加载,对象初始化就完成音频加载)
 		audioRef.current.play();
    setIsMusicPlay(true);
  }, [curIndex]);

情况2

Uncaught (in promise) DOMException: The play() request was interrupted by a call to pause().

**

出现的原因,上面链接的文章也解释了,就说play()是一个异步操作,要等播放完成操作,才能暂停播放

如果切歌频繁,这首歌还没有开始播,这首歌就要暂停了,然后就出现问题了

经历的步骤就是这样的

如何解决这个问题,文章中提到的,就不够实现我们的场景了,视频的播放和音频差不多,在promise.then中执行暂停操作,但这会有问题,我们是需要在点击之后就执行暂停上一首歌,播放下一首歌的。

import { useUpdateEffect, useMount, useThrottleFn } from 'ahooks';

// Handle setup when changing tracks
  useUpdateEffect(() => {
    
    audioRef.current = new Audio(musicList[curIndex].musicUrl); // 加载另一首音乐(使用对象加载,对象初始化就完成音频加载)
    handlePlay() // 开始播放
  }, [curIndex]);

const handlePlay = () => {
    if (isMusicPlay) {
      setIsMusicPlay(false)
      audioRef.current.pause()
    } else {
      setIsMusicPlay(true)
      const promise = audioRef.current.play()
      promise.catch((err: any) => {
        console.log(err)
        setIsMusicPlay(false)
      })
    }
  }

const handleNext = () => {
  handlePlay()
  setCurIndex(curIndex + 1)
}

<audio
  ref={audioRef}
  preload='auto'
  loop
  muted={false}
  onTimeUpdate={updateTime}
  onCanPlay={handleReady}
  className={styles.audioHidden} src={list[curIndex]}
/>

情况3

获取歌曲进度,音乐播放被停止

这个情况呢,是由于audio.currentTime为NaN了

所以,如果出现NaN的情况,就不要赋值给currentTime

const updateTime = (e: SyntheticEvent<HTMLAudioElement, Event>) => {
  setCurrentTime(Math.trunc(num))
}
  const changeProgressValue = (val: number) => {
    const num = Math.floor(val / 100 * audioRef.current.duration)
    if (!isNaN(num)) {
      audioRef.current.currentTime = num
    }
  }

<audio
  ref={audioRef}
  preload='auto'
  loop
  muted={false}
  onTimeUpdate={updateTime}
  onCanPlay={handleReady}
  className={styles.audioHidden} src={list[curIndex]}
/>
<MusicSlider
  className={styles.songSlider}
  defaultValue={currentTime && duration ? currentTime / duration * 100 : 0}
  onAfterChange={changeProgressValue}
  value={currentTime && duration ? currentTime / duration * 100 : 0}
/>

另外,由于使用Audio对象之后,audioRef.current对象发生变化,所以对应的timeupdate和canplay两个事件需要重新绑定

不然这两个事件在切歌之后将不会正常进行

// Handle setup when changing tracks
  useUpdateEffect(() => {
    isMusicPlay && audioRef.current.pause(); // 暂停音乐播放
    audioRef.current = new Audio(musicList[curIndex].musicUrl); // 加载另一首音乐(使用对象加载,对象初始化就完成音频加载)
    audioRef.current.removeEventListener('timeupdate', () => setCurrentTime(0)) // 取消上一首歌曲的绑定事件
    audioRef.current.removeEventListener('canplay', () => setDuration(0))

    audioRef.current.loop = true
    audioRef.current.addEventListener('timeupdate', updateTime) // 添加这一首歌曲的绑定事件(否则当前歌曲不会触发此事件)
    audioRef.current.addEventListener('canplay', handleReady)
  	handlePlay();
  }, [curIndex]);

页面移除,播放未停止

这个bug,完全就是我没有看完文档所导致的,罪魁祸首就说Audio这个对象

https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLAudioElement/Audio

audio这个对象都移除了,但是new的对象依然在内存中,不会销毁

所以就又回到了上一个问题的情况1

何时确保音乐资源加载完成再播放?

找一下audio标签的事件

https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/audio#事件

这个事件就能保证资源加载完成了,正好我在这个事件里面获取歌曲时长

const handleReady = () => {
    // @ts-ignore
    const promise = audioRef.current.play()
    promise.catch((err: any) => {
      console.log(err)
      setIsMusicPlay(false)
    })
    // @ts-ignore
    setDuration(audioRef.current.duration)
  }

有这个事件执行了,就不需要点击执行播放事件了

最终的播放逻辑代码

import { useUpdateEffect, useMount, useThrottleFn, useDocumentVisibility } from 'ahooks';
...
export default function MusicPlay(props: IMusicProps) {
	const router = useRouter();
  const [musicList, setMusicList] = useState<IDetailProp[]>(dataList)
  const [currentTime, setCurrentTime] = useState<null|number>(null)
  const [duration, setDuration] = useState<null|number>(null)
  const [curIndex, setCurIndex] = useState(0)
  const [isMusicPlay, setIsMusicPlay] = useState(false)
  const [isShowImgView, setShowImgView] = useState(false)
  const audioRef = useRef<HTMLAudioElement>(null)
  // 分享海报弹窗
  const [posterPopupShow, setPosterPopupShow] = useState(false);
  const documentVisibility = useDocumentVisibility()
  useMount(() => {

    audioRef.current.addEventListener('timeupdate', updateTime) // 添加这一首歌曲的绑定事件(否则当前歌曲不会触发此事件)
    audioRef.current.addEventListener('canplay', handleReadyInit)
    setDuration(audioRef.current.duration)
  });

  useEffect(() => {
    if (documentVisibility === 'hidden') {
      setIsMusicPlay(false)
      audioRef.current && audioRef.current.pause()
    }
  }, [documentVisibility])
  // Handle setup when changing tracks
  useUpdateEffect(() => {
    
    audioRef.current.pause(); // 暂停音乐播放
    // 不使用对象,因为对象在内存中,页面卸载时,无法移除
    // audioRef.current = new Audio(musicList[curIndex].musicUrl); // 加载另一首音乐(使用对象加载,对象初始化就完成音频加载)
    audioRef.current.removeEventListener('timeupdate', () => setCurrentTime(0)) // 取消上一首歌曲的绑定事件
    
    audioRef.current.removeEventListener('canplay', () => setDuration(0))
    // 重新绑定事件和属性,因为歌曲变化,歌曲的时长发生改变
   
    audioRef.current.loop = true
    
    audioRef.current.addEventListener('timeupdate', updateTime) // 添加这一首歌曲的绑定事件(否则当前歌曲不会触发此事件)
    
    audioRef.current.addEventListener('canplay', handleReady)

    setIsMusicPlay(true);
  }, [curIndex]);

  const { run } = useThrottleFn(
    (num) => {
      setCurrentTime(Math.trunc(num))
    },
    { wait: 1000 },
  );

  const updateTime = (e: SyntheticEvent<HTMLAudioElement, Event>) => {
   
    run(e.target.currentTime) // 节流更新
  }

  const handleReadyInit = () => {
    setDuration(audioRef.current.duration)
  }
  const handleReady = () => {
    const promise = audioRef.current.play()
    promise.catch((err: any) => {
      console.log(err)
      setIsMusicPlay(false)
    })
    setDuration(audioRef.current.duration)
  }

  const handlePlay = () => {
    if (isMusicPlay) {
      setIsMusicPlay(false)
      audioRef.current.pause()
    } else {
      setIsMusicPlay(true)
      const promise = audioRef.current.play()
      promise.catch((err: any) => {
        console.log(err)
        setIsMusicPlay(false)
      })
    }
  }


  const getMusicList = async(id: number, direction: number, isInit: boolean) => {
    showLoading(true)
    // 1-向前,2-向后
    const { data } = await musicListApi({
      baseId: id,
      direction: direction,
      size: 10
    })
    if (direction == 2) {
      setCurIndex(0)
    } else {
      setCurIndex(data.length - 1)
    }
    setMusicList(data)
    showLoading(false)
  }
  const changeProgressValue = (val: number) => {
 
    const num = Math.floor(val / 100 * audioRef.current.duration)
    if (!isNaN(num)) {
      audioRef.current.currentTime = num
    }
  }

  const handleNext = () => {
    if (musicList.length < 10) {
      if (curIndex < musicList.length - 1) {
        setCurIndex(curIndex + 1)
      } else {
        setCurIndex(0)
      }
    } else {
      if (curIndex < musicList.length - 1) {
        setCurIndex(curIndex + 1)
      } else {
        getMusicList(musicList[musicList.length - 1].id, 2, false)
      }
    }

  }

  const handlePrev = () => {
    if(musicList.length < 10) {
      if (curIndex - 1 < 0) {
        setCurIndex(musicList.length - 1)
      } else {
        setCurIndex(curIndex - 1)
      }
    } else {
      if (curIndex - 1 < 0) {
        getMusicList(musicList[0].id, 1, false)
      } else {
        setCurIndex(curIndex - 1)
      }
    }
  }

  return <>...
    <audio
      ref={audioRef}
      preload='auto'
      loop
      className={styles.audioHidden} src={musicList[curIndex].musicUrl || ''}
    />
    </>
}

这里是每次请求接口10首歌曲,如果歌曲小于10首,就循环播放.

今天的分享就到这里, 感谢阅读~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值