乐趣区

关于小程序:TaroReactTS基于InnerAudioContext封装一个基本的音频组件uniappvue后续更新

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

次要因为微信小程序官网的 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.tsx
import 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.ts
const 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.ts
import Taro from '@tarojs/taro'
import {setGlobalData, getGlobalData} from "./Global";

// 每次在一个音源暂停或者进行播放的时候将全局标识 audioPlaying 重置为 false,用以让后续的音频能够播放
export function afterAudioPlay() {setGlobalData('audioPlaying', false)
}

// 在每次播放音频之前查看全局变量 audioPlaying 是否为 true,如果是 true,以后音频不能播放,须要之前的音频完结或者手动去暂停或者进行之前的音频播放,如果是 false,返回 true,并将 audioPlaying 置为 true
export 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.tsx
import 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 & PageOwnProps

interface 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 版本的~

退出移动版