为什么要封装一个音频组件

次要因为微信小程序官网的audio不保护了,并且在很多iOS真机上的确也存在点击无奈播放,总时长不显示等问题.

音频组件的要求与限度

  1. 点击播放或者暂停
  2. 显示播放进度及总时长
  3. 通过图标变动显示以后音频所处状态(暂停/播放/加载中)
  4. 页面音频更新时刷新组件状态
  5. 全局有且只有一个音频处于播放状态
  6. 来到页面之后要主动进行播放并销毁音频实例

资料:

icon_loading.gif
icon_playing.png
icon_paused.png

InnerAudioContext提供的属性和办法

属性:

string src: 音频资源的地址,用于间接播放。
bumber startTime: 开始播放的地位(单位:s),默认为 0
boolean autoplay: 是否主动开始播放,默认为 false
boolean loop: 是否循环播放,默认为 false
number volume: 音量。范畴 0~1。默认为 1
number playbackRate: 播放速度。范畴 0.5-2.0,默认为 1。(Android 须要 6 及以上版本)
number duration: 以后音频的长度(单位 s)。只有在以后有非法的 src 时返回(只读)
number currentTime: 以后音频的播放地位(单位 s)。只有在以后有非法的 src 时返回,工夫保留小数点后 6 位(只读)
boolean paused: 以后是是否暂停或进行状态(只读)
number buffered: 音频缓冲的工夫点,仅保障以后播放工夫点到此工夫点内容已缓冲(只读)

办法

play(): 播放
pause(): 暂停。暂停后的音频再播放会从暂停处开始播放
stop(): 进行。进行后的音频再播放会从头开始播放。
seek(postions: number):跳转到指定地位
destory(): 销毁以后实例
onCanplay(callback): 监听音频进入能够播放状态的事件。但不保障前面能够晦涩播放
offCanplay(callback): 勾销监听音频进入能够播放状态的事件
onPlay(callback): 监听音频播放事件
offPlay(callback): 勾销监听音频播放事件
onPause(callback): 监听音频暂停事件
offPause(callback): 勾销监听音频暂停事件
onStop(callback): 监听音频进行事件
offStop(callback): 勾销监听音频进行事件
onEnded(callback): 监听音频天然播放至完结的事件
offEnded(callback): 勾销监听音频天然播放至完结的事件
onTimeUpdate(callback): 监听音频播放进度更新事件
offTimeUpdate(callback): 勾销监听音频播放进度更新事件
onError(callback): 监听音频播放谬误事件
offError(callbcak): 勾销监听音频播放谬误事件
onWaiting(callback): 监听音频加载中事件。当音频因为数据有余,须要停下来加载时会触发
offWaiting(callback): 勾销监听音频加载中事件
onSeeking(callback): 监听音频进行跳转操作的事件
offSeeking(callback): 勾销监听音频进行跳转操作的事件
onSeeked(callback): 监听音频实现跳转操作的事件
offSeeked(callback): 勾销监听音频实现跳转操作的事件

让咱们开始吧????

Taro(React + TS)

  • 首先构建一个简略的jsx构造:
<!-- playOrPauseAudio()是一个播放或者暂停播放音频的办法 --><!-- fmtSecond(time)是一个将秒格式化为 分:秒 的办法 --><View className='custom-audio'>  <Image onClick={() => this.playOrStopAudio()} src={audioImg} className='audio-btn' />  <Text>{this.fmtSecond(Math.floor(currentTime))}/{this.fmtSecond(Math.floor(duration))}</Text></View>
  • 定义组件承受的参数
type PageOwnProps = {  audioSrc: string // 传入的音频的src}
  • 定义CustomAudio组件的初始化相干的操作,并给innerAudioContext的回调增加一写行为
// src/components/widget/CustomAudio.tsximport Taro, { Component, ComponentClass } from '@tarojs/taro'import { View, Image, Text } from "@tarojs/components";import iconPaused from '../../../assets/images/icon_paused.png'import iconPlaying from '../../../assets/images/icon_playing.png'import iconLoading from '../../../assets/images/icon_loading.gif'interface StateInterface {  audioCtx: Taro.InnerAudioContext // innerAudioContext实例  audioImg: string // 以后音频icon标识  currentTime: number // 以后播放的工夫  duration: number // 以后音频总时长}class CustomAudio extends Component<{}, StateInterface> {  constructor(props) {    super(props)    this.fmtSecond = this.fmtSecond.bind(this)    this.state = {      audioCtx: Taro.createInnerAudioContext(),      audioImg: iconLoading, // 默认是在加载音频中的状态      currentTime: 0,      duration: 0    }  }  componentWillMount() {    const {      audioCtx,      audioImg    } = this.state    audioCtx.src = this.props.audioSrc    // 当播放的时候通过TimeUpdate的回调去更改以后播放时长和总时长(总时长更新放到onCanplay回调中会出错)    audioCtx.onTimeUpdate(() => {      if (audioCtx.currentTime > 0 && audioCtx.currentTime <= 1) {        this.setState({          currentTime: 1        })      } else if (audioCtx.currentTime !== Math.floor(audioCtx.currentTime)) {        this.setState({          currentTime: Math.floor(audioCtx.currentTime)        })      }      const tempDuration = Math.ceil(audioCtx.duration)      if (this.state.duration !== tempDuration) {        this.setState({          duration: tempDuration        })      }      console.log('onTimeUpdate')    })    // 当音频能够播放就将状态从loading变为可播放    audioCtx.onCanplay(() => {      if (audioImg === iconLoading) {        this.setAudioImg(iconPaused)        console.log('onCanplay')      }    })    // 当音频在缓冲时扭转状态为加载中    audioCtx.onWaiting(() => {      if (audioImg !== iconLoading) {        this.setAudioImg(iconLoading)      }    })    // 开始播放后更改图标状态为播放中    audioCtx.onPlay(() => {      console.log('onPlay')      this.setAudioImg(iconPlaying)    })    // 暂停后更改图标状态为暂停    audioCtx.onPause(() => {      console.log('onPause')      this.setAudioImg(iconPaused)    })    // 播放完结后更改图标状态    audioCtx.onEnded(() => {      console.log('onEnded')      if (audioImg !== iconPaused) {        this.setAudioImg(iconPaused)      }    })    // 音频加载失败时 抛出异样    audioCtx.onError((e) => {      Taro.showToast({        title: '音频加载失败',        icon: 'none'      })      throw new Error(e.errMsg)    })  }  setAudioImg(newImg: string) {    this.setState({      audioImg: newImg    })  }  // 播放或者暂停  playOrStopAudio() {    const audioCtx = this.state.audioCtx    if (audioCtx.paused) {      audioCtx.play()    } else {      audioCtx.pause()    }  }  fmtSecond (time: number){    let hour = 0    let min = 0    let second = 0       if (typeof time !== 'number') {         throw new TypeError('必须是数字类型')      } else {        hour = Math.floor(time / 3600) >= 0 ? Math.floor(time / 3600) : 0,        min = Math.floor(time % 3600 / 60) >= 0 ? Math.floor(time % 3600 / 60) : 0,        second = Math.floor(time % 3600 % 60) >=0 ? Math.floor(time % 3600 % 60) : 0      }    }    return `${hour}:${min}:${second}`  }    render () {    const {      audioImg,      currentTime,      duration    } = this.state    return(      <View className='custom-audio'>        <Image onClick={() => this.playOrStopAudio()} src={audioImg} className='audio-btn' />        <Text>{this.fmtSecond(Math.floor(currentTime))}/{this.fmtSecond(Math.floor(duration))}</Text>      </View>    )  }}export default CustomAudio as ComponentClass<PageOwnProps, PageState>

问题

乍一看咱们的组件曾经满足了

  1. 点击播放或者暂停
  2. 显示播放进度及总时长
  3. 通过图标变动显示以后音频所处状态(暂停/播放/加载中)

然而这个组件还有一些问题:

  1. 页面卸载之后没有对innerAudioContext对象进行播放和回收
  2. 一个页面如果有多个音频组件这些组件能够同时播放这会导致音源凌乱,性能升高
  3. 因为是在ComponentWillMount中初始化了innerAudioContext的属性所以当props中的audioSrc变动的时候组件自身不会更新音源、组件的播放状态和播放时长

改良

componentWillReceiveProps中减少一些行为达到props中的audioSrc更新时组件的音源也做一个更新,播放时长和状态也做一个更新

componentWillReceiveProps(nextProps) {  const newSrc = nextProps.audioSrc || ''  console.log('componentWillReceiveProps', nextProps)  if (this.props.audioSrc !== newSrc && newSrc !== '') {    const audioCtx = this.state.audioCtx    if (!audioCtx.paused) { // 如果还在播放中,先进行进行播放操作        audioCtx.stop()    }    audioCtx.src = nextProps.audioSrc    // 重置以后播放工夫和总时长    this.setState({      currentTime: 0,      duration: 0,    })  }}

这时候咱们在切换音源的时候就不会存在还在播放旧音源的问题

通过在componentWillUnmount中进行播放和销毁innerAudioContext达到一个晋升性能的目标

componentWillUnmount() {  console.log('componentWillUnmount')  this.state.audioCtx.stop()  this.state.audioCtx.destory()}

通过一个全局变量audioPlaying来保障全局有且仅有一个音频组件能够处于播放状态

// 在Taro中定义全局变量依照一下的标准来,获取和更改数据也要应用定义的get和set办法,间接通过Taro.getApp()是不行的// src/lib/Global.tsconst globalData = {  audioPlaying: false, // 默认没有音频组件处于播放状态}export function setGlobalData (key: string, val: any) {  globalData[key] = val}export function getGlobalData (key: string) {  return globalData[key]}

咱们通过封装两个函数去判断是否能够播放以后音源:beforeAudioPlayafterAudioPlay

// src/lib/Util.tsimport Taro from '@tarojs/taro'import { setGlobalData, getGlobalData } from "./Global";// 每次在一个音源暂停或者进行播放的时候将全局标识audioPlaying重置为false,用以让后续的音频能够播放export function afterAudioPlay() {  setGlobalData('audioPlaying', false)}// 在每次播放音频之前查看全局变量audioPlaying是否为true,如果是true,以后音频不能播放,须要之前的音频完结或者手动去暂停或者进行之前的音频播放,如果是false,返回true,并将audioPlaying置为trueexport function beforeAudioPlay() {  const audioPlaying = getGlobalData('audioPlaying')  if (audioPlaying) {    Taro.showToast({      title: '请先暂停其余音频播放',      icon: 'none'    })    return false  } else {    setGlobalData('audioPlaying', true)    return true  }}

接下来咱们革新之前的CustomAudio组件

import { beforeAudioPlay, afterAudioPlay } from '../../lib/Utils';/* ... */// 因为组件卸载导致的进行播放别忘了也要扭转全局audioPlaying的状态componentWillUnmount() {  console.log('componentWillUnmount')  this.state.audioCtx.stop()  this.state.audioCtx.destory()  ++ afterAudioPlay()}/* ... */// 每次暂停或者播放结束的时候须要执行一次afterAudioPlay()让出播放音频的机会给其余的音频组件audioCtx.onPause(() => {  console.log('onPause')  this.setAudioImg(iconPaused)  ++ afterAudioPlay()})audioCtx.onEnded(() => {  console.log('onEnded')  if (audioImg !== iconPaused) {    this.setAudioImg(iconPaused)  }  ++ afterAudioPlay()})/* ... */// 播放前先查看有没有其余正在播放的音频,没有的状况下能力播放以后音频playOrStopAudio() {  const audioCtx = this.state.audioCtx  if (audioCtx.paused) {    ++ if (beforeAudioPlay()) {      audioCtx.play()    ++ }  } else {    audioCtx.pause()  }}

最终代码

// src/components/widget/CustomAudio.tsximport Taro, { Component, ComponentClass } from '@tarojs/taro'import { View, Image, Text } from "@tarojs/components";import { beforeAudioPlay, afterAudioPlay } from '../../lib/Utils';import './CustomAudio.scss'import iconPaused from '../../../assets/images/icon_paused.png'import iconPlaying from '../../../assets/images/icon_playing.png'import iconLoading from '../../../assets/images/icon_loading.gif'type PageStateProps = {}type PageDispatchProps = {}type PageOwnProps = {  audioSrc: string}type PageState = {}type IProps = PageStateProps & PageDispatchProps & PageOwnPropsinterface CustomAudio {  props: IProps}interface StateInterface {  audioCtx: Taro.InnerAudioContext  audioImg: string  currentTime: number  duration: number}class CustomAudio extends Component<{}, StateInterface> {  constructor(props) {    super(props)    this.fmtSecond = this.fmtSecond.bind(this)    this.state = {      audioCtx: Taro.createInnerAudioContext(),      audioImg: iconLoading,      currentTime: 0,      duration: 0    }  }  componentWillMount() {    const {      audioCtx,      audioImg    } = this.state    audioCtx.src = this.props.audioSrc    // 当播放的时候通过TimeUpdate的回调去更改以后播放时长和总时长(总时长更新放到onCanplay回调中会出错)    audioCtx.onTimeUpdate(() => {      if (audioCtx.currentTime > 0 && audioCtx.currentTime <= 1) {        this.setState({          currentTime: 1        })      } else if (audioCtx.currentTime !== Math.floor(audioCtx.currentTime)) {        this.setState({          currentTime: Math.floor(audioCtx.currentTime)        })      }      const tempDuration = Math.ceil(audioCtx.duration)      if (this.state.duration !== tempDuration) {        this.setState({          duration: tempDuration        })      }      console.log('onTimeUpdate')    })    // 当音频能够播放就将状态从loading变为可播放    audioCtx.onCanplay(() => {      if (audioImg === iconLoading) {        this.setAudioImg(iconPaused)        console.log('onCanplay')      }    })    // 当音频在缓冲时扭转状态为加载中    audioCtx.onWaiting(() => {      if (audioImg !== iconLoading) {        this.setAudioImg(iconLoading)      }    })    // 开始播放后更改图标状态为播放中    audioCtx.onPlay(() => {      console.log('onPlay')      this.setAudioImg(iconPlaying)    })    // 暂停后更改图标状态为暂停    audioCtx.onPause(() => {      console.log('onPause')      this.setAudioImg(iconPaused)      afterAudioPlay()    })    // 播放完结后更改图标状态    audioCtx.onEnded(() => {      console.log('onEnded')      if (audioImg !== iconPaused) {        this.setAudioImg(iconPaused)      }      afterAudioPlay()    })    // 音频加载失败时 抛出异样    audioCtx.onError((e) => {      Taro.showToast({        title: '音频加载失败',        icon: 'none'      })      throw new Error(e.errMsg)    })  }  componentWillReceiveProps(nextProps) {      const newSrc = nextProps.audioSrc || ''    console.log('componentWillReceiveProps', nextProps)    if (this.props.audioSrc !== newSrc && newSrc !== '') {      const audioCtx = this.state.audioCtx      if (!audioCtx.paused) { // 如果还在播放中,先进行进行播放操作        audioCtx.stop()      }      audioCtx.src = nextProps.audioSrc      // 重置以后播放工夫和总时长      this.setState({        currentTime: 0,        duration: 0,      })    }  }  componentWillUnmount() {    console.log('componentWillUnmount')    this.state.audioCtx.stop()    this.state.audioCtx.destory()    afterAudioPlay()  }  setAudioImg(newImg: string) {    this.setState({      audioImg: newImg    })  }  playOrStopAudio() {    const audioCtx = this.state.audioCtx    if (audioCtx.paused) {      if (beforeAudioPlay()) {        audioCtx.play()      }    } else {      audioCtx.pause()    }  }  fmtSecond (time: number){    let hour = 0    let min = 0    let second = 0       if (typeof time !== 'number') {         throw new TypeError('必须是数字类型')      } else {        hour = Math.floor(time / 3600) >= 0 ? Math.floor(time / 3600) : 0,        min = Math.floor(time % 3600 / 60) >= 0 ? Math.floor(time % 3600 / 60) : 0,        second = Math.floor(time % 3600 % 60) >=0 ? Math.floor(time % 3600 % 60) : 0      }    }    return `${hour}:${min}:${second}`  }  render () {    const {      audioImg,      currentTime,      duration    } = this.state    return(      <View className='custom-audio'>        <Image onClick={() => this.playOrStopAudio()} src={audioImg} className='audio-btn' />        <Text>{this.fmtSecond(Math.floor(currentTime))}/{this.fmtSecond(Math.floor(duration))}</Text>      </View>    )  }}export default CustomAudio as ComponentClass<PageOwnProps, PageState>

提供一份款式文件,也能够本人自行施展

// src/components/widget/CustomAudio.scss.custom-audio {  border-radius: 8vw;  border: #CCC 1px solid;  background: #F3F6FC;  color: #333;  display: flex;  flex-flow: row nowrap;  align-items: center;  justify-content: space-between;  padding: 2vw;  font-size: 4vw;  .audio-btn {    width: 10vw;    height: 10vw;    white-space: nowrap;    display: flex;    align-items: center;    justify-content: center;  }}

最终成果~

★,°:.☆( ̄▽ ̄)/$:*.°★* 。完满*★,°*:.☆( ̄▽ ̄)/$:.°★ 。????????????

有什么好的倡议大家能够在评论区跟我探讨下哈,别忘了点赞珍藏分享哦,下期就更uni-app版本的~